aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorgjoranv <gjoranv@gmail.com>2023-11-03 18:08:32 +0100
committergjoranv <gjoranv@gmail.com>2023-11-06 00:31:08 +0100
commit596421557e3165ef25dd478edf64b2812d5b4777 (patch)
tree36ed938c7fe0519caf83cbb798d64bd98aa8aa0e
parentc5d8e300da1bee0cff8e83a3c0a4b9a9a4fa8375 (diff)
More controller code to internal repo.
-rw-r--r--controller-api/OWNERS2
-rw-r--r--controller-api/README1
-rw-r--r--controller-api/pom.xml147
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/ApplicationApi.java30
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/ApplicationResource.java44
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/CostResource.java30
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/EnvironmentResource.java89
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/TenantResource.java45
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/ApplicationReference.java16
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/ClusterMetrics.java76
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostItem.java38
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostItemUsage.java22
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostMonths.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostResult.java22
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java164
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentEndpoints.java25
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentReference.java20
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/EndpointStatus.java45
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstanceInformation.java59
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstanceReference.java23
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstancesReply.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/JsonResponse.java20
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/LogEntry.java35
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/SearchNodeMetrics.java68
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantCreateOptions.java40
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantInfo.java31
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantMetaData.java32
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantType.java11
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantUpdateOptions.java52
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantWithApplications.java35
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/Environment.java38
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/Region.java39
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ApplicationId.java25
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ClusterId.java50
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ControllerVersion.java68
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/DeploymentId.java67
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/EnvironmentId.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitBranch.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitCommit.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitRepository.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java104
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/InstanceId.java25
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/MetricsType.java24
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/NonDefaultIdentifier.java21
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Property.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/PropertyId.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RegionId.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RevisionId.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ScrewdriverId.java30
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/SerializedIdentifier.java22
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/TenantId.java25
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/UserGroup.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/UserId.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrls.java94
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ControllerIdentityProvider.java16
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/LogEntry.java122
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/RunDataStore.java39
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java131
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBuckets.java34
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java48
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveUriUpdate.java43
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java79
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/TenantManagedArchiveBucket.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/VespaManagedArchiveBucket.java72
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/package-info.java8
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/Artifact.java84
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/ArtifactRegistry.java20
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/package-info.java8
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AccessControlService.java26
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ApplicationAction.java17
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzAccessControlService.java161
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzClientFactory.java21
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzClientFactoryMock.java45
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java298
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizer.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizerMock.java12
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/MockAccessControlService.java65
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java362
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZtsClientMock.java118
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/package-info.java8
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/EnclaveAccessService.java19
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockEnclaveAccessService.java24
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockResourceTagger.java28
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockRoleService.java22
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/NoopRoleService.java43
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/ResourceTagger.java27
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/RoleService.java32
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/TenantRoles.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/AcceptedCountries.java23
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java488
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatus.java46
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java114
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java158
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java195
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java17
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java89
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionMethod.java9
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionResult.java55
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java32
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java67
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java85
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java212
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java91
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java43
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java34
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java168
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanResult.java55
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java95
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/QuotaCalculator.java12
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistory.java61
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificate.java161
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateDetails.java31
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateException.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateProvider.java21
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateProviderMock.java94
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateRequest.java18
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidator.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java81
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorMock.java22
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Application.java35
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ApplicationReindexing.java176
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ApplicationStats.java28
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ArchiveUris.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Cluster.java198
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java167
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServerException.java67
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServerVersion.java32
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ContainerEndpoint.java43
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/DeploymentResult.java30
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/FlagsV1Api.java55
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Load.java41
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java46
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Log.java16
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Node.java812
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeFilter.java130
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepoStats.java32
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepository.java90
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NotFoundException.java11
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/PrepareResponse.java17
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ProxyResponse.java33
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/QuotaUsage.java11
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/QuotaUsageResponse.java12
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ServiceConvergence.java61
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/TargetVersions.java67
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneToken.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneTokenVersions.java17
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/FingerPrint.java23
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/TokenId.java23
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationStore.java93
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java229
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ArtifactRepository.java21
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobId.java45
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java234
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/OsRelease.java54
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RevisionId.java64
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RunId.java51
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/SourceRevision.java48
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TestReport.java23
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TesterCloud.java82
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TesterId.java33
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java68
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java57
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java67
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java66
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java124
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MockVpcEndpointService.java56
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java60
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java80
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordData.java43
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordName.java49
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/VpcEndpointService.java55
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java70
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java70
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/EntityService.java26
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/MemoryEntityService.java41
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/NodeEntity.java49
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/HorizonClient.java19
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/HorizonResponse.java36
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/MockHorizonClient.java34
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/Jira.java19
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraComment.java20
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraCreateIssue.java86
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraIssue.java45
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraIssues.java22
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/ArtifactId.java30
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/MavenRepository.java17
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/Metadata.java59
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationData.java30
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationPatch.java61
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationStatsData.java33
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ArchiveList.java25
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ArchivePatch.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/AutoscalingData.java52
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/AutoscalingMetricsData.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/Capacity.java20
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ClusterData.java64
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ClusterResourcesData.java30
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/IntRangeData.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/LoadData.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/MaintenanceJobList.java17
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/MaintenanceJobName.java11
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeHistory.java36
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeList.java27
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeMembership.java43
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeOwner.java50
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeRepoStatsData.java40
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeRepositoryNode.java533
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeResources.java164
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeState.java20
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeTargetVersions.java37
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeType.java34
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeUpgrade.java45
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ProvisionResource.java134
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/RestartFilter.java51
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ScalingEventData.java41
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/package-info.java8
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/AccountId.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ApplicationSummary.java81
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/BillingInfo.java55
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Contact.java90
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ContactRetriever.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentFailureMails.java81
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentIssues.java24
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Issue.java120
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueHandler.java138
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueId.java49
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueInfo.java65
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Mail.java46
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Mailer.java22
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MailerException.java14
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockContactRetriever.java37
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockIssueHandler.java194
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/OwnershipIssues.java44
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ProjectInfo.java31
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/SystemMonitor.java20
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/User.java52
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/repair/RepairTicketReport.java62
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/repair/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostInfo.java106
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostReportConsumer.java17
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostReportConsumerMock.java37
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceAllocation.java77
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java62
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java162
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java137
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceUsage.java78
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/routing/RotationStatus.java11
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/routing/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/EndpointSecretManager.java6
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/GcpSecretStore.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopEndpointSecretManager.java9
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopGcpSecretStore.java21
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopTenantSecretService.java21
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/TenantSecretService.java19
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/TenantSecretStore.java62
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/DummyOwnershipIssues.java32
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/DummySystemMonitor.java15
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingDeploymentIssues.java92
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMailer.java59
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMavenRepository.java58
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockRunDataStore.java68
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockTesterCloud.java102
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockUserManagement.java106
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/package-info.java11
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleMaintainer.java20
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleMaintainerMock.java30
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/Roles.java102
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserId.java41
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserManagement.java50
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserSessionManager.java13
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequest.java146
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestClient.java16
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestSource.java85
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/HostAction.java74
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/MockChangeRequestClient.java33
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VcmrReport.java101
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VespaChangeRequest.java103
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java106
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Action.java43
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/ApplicationRole.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Context.java85
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Enforcer.java39
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java354
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java214
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Privilege.java99
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java112
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java129
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java79
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SimplePrincipal.java47
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/TenantRole.java25
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/UnboundRole.java20
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/package-info.java5
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ConfigServerFlagsTarget.java71
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ControllerFlagsTarget.java64
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagValidationException.java11
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTarget.java153
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java376
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/package-info.java8
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/SystemFlagsV1Api.java29
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/WireErrorResponse.java18
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/WireSystemFlagsDeployResult.java57
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/package-info.java8
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java118
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java75
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/BillingReference.java7
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudAccountInfo.java19
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java111
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java40
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Email.java60
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java55
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PendingMailVerification.java82
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PurchaseOrder.java22
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TaxId.java51
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java101
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantAddress.java98
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java109
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContact.java69
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContacts.java164
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java130
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TermsOfServiceApproval.java30
-rw-r--r--controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java8
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/identifiers/IdentifierTest.java169
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrlsTest.java44
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatusTest.java19
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistoryTest.java89
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateTest.java31
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobTypeTest.java51
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTargetTest.java39
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java36
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/maven/MetadataTest.java60
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/user/RolesTest.java74
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/resource/ResourceSnapshotTest.java51
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/PathGroupTest.java73
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java181
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTargetTest.java41
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java487
-rw-r--r--controller-api/src/test/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccessTest.java46
-rw-r--r--controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/cd.controller.json8
-rw-r--r--controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/default.json8
-rw-r--r--controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/main.prod.us-west-1.json8
-rw-r--r--controller-api/src/test/resources/system-flags-multi-level-with-duplicated-flagdata/flags/group-1/my-test-flag/default.json8
-rw-r--r--controller-api/src/test/resources/system-flags-multi-level-with-duplicated-flagdata/flags/group-2/my-test-flag/default.json8
-rw-r--r--controller-api/src/test/resources/system-flags-multi-level/flags/group-1/my-test-flag/default.json8
-rw-r--r--controller-api/src/test/resources/system-flags-multi-level/flags/group-2/my-other-test-flag/default.json8
-rw-r--r--controller-api/src/test/resources/system-flags-with-invalid-file-name/flags/my-test-flag/file-name-without-dot-json8
-rw-r--r--controller-api/src/test/resources/system-flags-with-null-value/flags/my-test-flag/main.prod.us-east-3.json24
-rw-r--r--controller-api/src/test/resources/system-flags-with-null-value/flags/my-test-flag/main.prod.us-west-1.json25
-rw-r--r--controller-api/src/test/resources/system-flags-with-unknown-field-name/flags/my-test-flag/main.prod.us-west-1.json15
-rw-r--r--controller-api/src/test/resources/system-flags-with-unknown-file-name/flags/my-test-flag/main.prod.unknown-region.json8
-rw-r--r--controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/default.json8
-rw-r--r--controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.controller.json4
-rw-r--r--controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.json10
-rw-r--r--controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.prod.json0
-rw-r--r--controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.prod.us-west-1.json8
-rw-r--r--controller-api/src/test/resources/system-flags/flags/my-test-flag/default.json8
-rw-r--r--controller-api/src/test/resources/system-flags/flags/my-test-flag/main.controller.json8
-rw-r--r--controller-api/src/test/resources/system-flags/flags/my-test-flag/main.json8
-rw-r--r--controller-api/src/test/resources/system-flags/flags/my-test-flag/main.prod.json8
-rw-r--r--controller-api/src/test/resources/system-flags/flags/my-test-flag/main.prod.us-west-1.json8
-rw-r--r--controller-server/OWNERS2
-rw-r--r--controller-server/README1
-rw-r--r--controller-server/pom.xml284
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java254
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java1111
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java306
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java235
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java193
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java308
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/NotExistsException.java33
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/OsController.java217
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java624
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java228
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationActivity.java78
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java61
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java35
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java185
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java129
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentActivity.java97
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java138
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java87
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java658
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointId.java58
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java111
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/GeneratedEndpoint.java59
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java190
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java139
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java63
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java98
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java103
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/package-info.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java318
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageDiff.java112
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageStream.java246
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java270
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/BasicServicesXml.java102
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/LinesComparator.java246
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java384
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java77
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDb.java126
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/HostedAthenzIdentities.java27
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/config/package-info.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzClientFactoryImpl.java75
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java372
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLog.java147
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLogger.java119
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLoggingRequestHandler.java36
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/package-info.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/AssignedCertificate.java33
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java318
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/UnassignedCertificate.java39
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/concurrent/Once.java46
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/ConvergenceSummary.java146
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java1285
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatusList.java57
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java505
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java1063
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java932
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java221
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java71
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobProfile.java98
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java107
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/LockedStep.java16
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeList.java118
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeWithServices.java102
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntry.java27
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntrySerializer.java65
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java147
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java353
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunList.java44
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunLog.java58
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java53
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java122
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/StepInfo.java61
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/StepRunner.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Submission.java60
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java97
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java156
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/ZipBuilder.java65
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/package-info.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/AbstractNameServiceRequest.java34
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecord.java67
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java87
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java93
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceQueue.java189
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceRequest.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java94
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationMetaDataGarbageCollector.java36
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java178
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java75
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java104
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArtifactExpirer.java130
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdater.java321
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingDatabaseMaintainer.java24
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainer.java135
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CertificatePoolMaintainer.java140
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessor.java267
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java136
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudAccountVerifier.java56
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudDatabaseMaintainer.java27
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java273
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java70
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainer.java72
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java221
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainer.java40
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DataPlaneTokenRedeployer.java24
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java62
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentInfoMaintainer.java67
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java157
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java129
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java126
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainer.java42
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java258
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/HostInfoUpdater.java93
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/InfrastructureUpgrader.java174
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java196
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MeteringMonitorMaintainer.java65
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java388
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcher.java70
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java251
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgrader.java121
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdater.java34
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java43
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReadyJobsTrigger.java27
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReindexingTriggerer.java84
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java300
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceTagMaintainer.java76
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RetriggerMaintainer.java69
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgrader.java97
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleCleanupMaintainer.java31
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainer.java55
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java211
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainer.java53
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainer.java399
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdater.java61
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/package-info.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/metric/ApplicationMetrics.java33
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/metric/CostCalculator.java89
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/FormattedNotification.java21
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java103
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MissingOptionalException.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java133
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationFormatter.java127
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationSource.java152
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java265
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java172
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/package-info.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java575
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializer.java91
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializer.java110
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java151
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CertifiedOsVersionSerializer.java50
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializer.java157
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ConfidenceOverrideSerializer.java48
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java44
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java948
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DataplaneTokenSerializer.java74
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DnsChallengeSerializer.java72
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateSerializer.java91
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/JobControlFlags.java37
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializer.java120
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MailVerificationSerializer.java51
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MockCuratorDb.java41
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializer.java134
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java58
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java178
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionSerializer.java61
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializer.java67
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionTargetSerializer.java64
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializer.java157
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java418
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializer.java140
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java580
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TrialNotifications.java57
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/UnassignedCertificateSerializer.java32
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java121
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ZoneRoutingPolicySerializer.java44
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/package-info.java10
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutor.java16
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java301
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java87
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java46
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/package-info.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/ErrorResponses.java31
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java3479
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/HtmlResponse.java30
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java533
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java127
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ZipResponse.java37
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java110
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java683
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/CsvResponse.java39
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/certificate/EndpointCertificatesHandler.java101
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java307
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java125
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AccessRequestResponse.java28
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java34
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java200
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/DecryptionTokenResealer.java74
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/JobsResponse.java37
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/MeteringResponse.java43
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/RequestUtils.java29
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ResealedTokenResponse.java27
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/StatsResponse.java57
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java32
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandler.java50
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java261
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java163
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java310
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/CliApiHandler.java63
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java300
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java226
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java60
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java73
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java101
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.java30
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java169
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java113
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java265
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java374
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java202
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClientException.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java431
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java225
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java83
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java417
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.java85
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java118
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java106
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java5
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/EndpointConfig.java30
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/GeneratedEndpointList.java58
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/PreparedEndpoints.java115
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingId.java34
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java781
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java121
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicyId.java34
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicyList.java112
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingStatus.java71
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/ZoneRoutingPolicy.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/DeploymentRoutingContext.java162
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/ExclusiveZoneRoutingContext.java41
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/RoutingContext.java23
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/SharedZoneRoutingContext.java47
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/Rotation.java35
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationId.java21
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationLock.java26
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationRepository.java132
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationState.java20
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationStatus.java67
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java68
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControlRequests.java21
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzAccessControlRequests.java69
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzCredentials.java37
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzTenantSpec.java40
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Auth0Credentials.java30
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java136
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControlRequests.java40
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudTenantSpec.java25
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManager.java50
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Credentials.java24
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/TenantSpec.java25
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccess.java126
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessChange.java53
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java104
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessGrant.java36
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/ControllerSslContextFactoryProvider.java101
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/package-info.java8
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/CertifiedOsVersion.java29
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java145
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/MavenRepositoryClient.java65
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java51
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersion.java35
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java95
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionTarget.java36
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java285
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionTarget.java19
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java178
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersionTarget.java24
-rw-r--r--controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/package-info.java5
-rw-r--r--controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.athenz.config.athenz.def15
-rw-r--r--controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.controller.def7
-rw-r--r--controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.core-dump-token-resealing.def6
-rw-r--r--controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.well-known-folder.def5
-rw-r--r--controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.maven.repository.config.maven-repository.def15
-rw-r--r--controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.tls.config.tls.def9
-rw-r--r--controller-server/src/main/resources/configdefinitions/vespa.hosted.rotation.config.rotations.def4
-rw-r--r--controller-server/src/main/resources/mail/default-mail-content.vm131
-rw-r--r--controller-server/src/main/resources/mail/mail-verification.vm494
-rw-r--r--controller-server/src/main/resources/mail/mail.vm516
-rw-r--r--controller-server/src/main/resources/mail/notification-message.vm6
-rw-r--r--controller-server/src/main/resources/mail/trial-expired.vm3
-rw-r--r--controller-server/src/main/resources/mail/trial-expires-immediately.vm3
-rw-r--r--controller-server/src/main/resources/mail/trial-midway-checkin.vm3
-rw-r--r--controller-server/src/main/resources/mail/trial-signed-up.vm3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java1584
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java403
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/MailVerifierTest.java128
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java116
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java396
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageDiffTest.java127
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java310
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/BasicServicesXmlTest.java60
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/LinesComparatorTest.java112
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java288
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/response/application.json189
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDbTest.java91
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLoggerTest.java109
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java486
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/concurrent/OnceTest.java27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java444
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java669
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java154
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java3146
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java566
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/QuotaUsageTest.java21
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java54
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ZipBuilderTest.java63
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/dns/NameServiceQueueTest.java211
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java158
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ArtifactRegistryMock.java39
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ArtifactRepositoryMock.java41
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/AthenzFilterMock.java57
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java634
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerProxyMock.java42
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/MetricsMock.java110
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java378
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/SecretStoreMock.java53
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java332
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneApiMock.java143
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneFilterMock.java113
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java283
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java125
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java65
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java102
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArtifactExpirerTest.java118
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdaterTest.java316
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainerTest.java199
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CertificatePoolMaintainerTest.java56
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessorTest.java188
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java113
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java185
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainerTest.java69
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainerTest.java81
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainerTest.java62
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java88
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentInfoMaintainerTest.java51
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java234
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java137
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgraderTest.java86
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainerTest.java43
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainerTest.java285
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/HostInfoUpdaterTest.java129
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java581
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MeteringMonitorMaintainerTest.java53
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java685
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcherTest.java138
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeSchedulerTest.java254
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgraderTest.java392
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdaterTest.java95
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java59
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ReindexingTriggererTest.java84
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java178
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceTagMaintainerTest.java79
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RetriggerMaintainerTest.java69
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgraderTest.java479
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainerTest.java102
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java1168
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainerTest.java58
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainerTest.java320
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdaterTest.java45
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationFormatterTest.java92
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java322
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java89
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java262
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializerTest.java28
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializerTest.java68
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStoreTest.java135
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/CertifiedOsVersionSerializerTest.java31
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializerTest.java46
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializerTest.java28
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/DnsChallengeSerializerTest.java46
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateSerializerTest.java52
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializerTest.java64
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/MailVerificationSerializerTest.java31
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java65
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java75
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionSerializerTest.java30
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializerTest.java47
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionTargetSerializerTest.java33
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializerTest.java88
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java160
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializerTest.java170
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java359
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/UnassignedCertificateSerializerTest.java30
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java64
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ZoneRoutingPolicySerializerTest.java29
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json332
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/logs.json32
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/run-status.json67
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImplTest.java120
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java67
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java57
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java148
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java236
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java102
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java190
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ResponseHandlerToApplicationResponseWrapper.java77
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java858
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java2048
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/CliApiHandlerTest.java26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java244
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParserTest.java116
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json143
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-list.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-nodes.json28
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-instances.json12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json41
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json38
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-result.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-cloud.json43
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-job-accepted-2.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview-2.json1334
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview.json763
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-with-routing-policy.json42
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-without-shared-endpoints.json42
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json77
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-aws-us-east-2a-runs.json32
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-overview.json69
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1-log-first-part.json87
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1-log-second-part.json44
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-delete.json3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-get.json11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-put.json3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation.json5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference-2.json6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference-default.json6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference.json6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-with-routing-policy.json24
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json88
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json189
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs-direct-deployment.json3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json71
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-applicationPackage.json18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1-app2.json17
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1.json27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview-enclave.json305
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview-user-instance.json37
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview.json3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/proton-metrics.json26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json228
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-until-tenant-root.json45
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/root.json7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/search-deployments-multi.json31
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/search-deployments-single.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/service1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/staging-runs.json427
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/staging-test-log.json243
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/suspended.json3
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json412
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json129
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-log.json415
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-cloud.json35
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-application.json17
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-empty-application.json16
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-with-id.json11
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications.json10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-deleted.json9
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json202
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1.json10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json24
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config-dev.json33
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json24
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/us-east-3-log-without-first.json27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/vespa.log1
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java57
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/property-list.json12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/root.json10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java293
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/accepted-countries.json36
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java142
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/initial.json27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/patched-vcmr.json32
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmr.json37
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmrs.json41
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java146
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/responses/root.json29
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java308
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandlerTest.java51
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/auditlog.json19
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json142
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/metering.json26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/root.json19
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/stats.json68
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenServiceTest.java225
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java100
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java118
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history.svg183
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history2.svg183
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg119
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json380
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/running-test.svg93
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/single-done.svg87
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/single-running.svg87
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java136
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java137
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java56
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java148
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java57
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java65
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java53
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiTest.java214
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json162
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json175
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/AllowingFilter.java41
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/DeploymentPlayground.java327
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment.xml133
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_alt_full.xml133
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_alternative.xml68
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_base.xml64
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_full.xml128
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simple.xml45
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simpler.xml31
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simplest.xml40
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java343
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/application.json7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/environment.json46
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/instance.json10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/root.json10
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/tenant.json7
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/endpoint/endpoints.json31
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-in.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-initial.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-out.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-in.json8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-initial.json8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-out.json8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/application.json22
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/environment.json116
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/tenant.json40
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-in.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-initial.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-out.json13
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-in.json8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-initial.json8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-out.json8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResultTest.java76
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployerTest.java258
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java81
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java344
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializerTest.java133
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-created.json5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json12
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json5
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/on-prem-user-without-applications.json27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json8
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-roles.json48
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json36
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json44
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json21
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json47
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json42
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-supported-tenant.json35
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json22
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java69
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/default-for-region.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/no-default-region.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/prod.json6
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/root.json18
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java106
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json26
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json4
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java1683
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationRepositoryTest.java205
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManagerTest.java64
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/Keys.java33
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecretStoreMock.java27
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecureContainerTest.java94
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/MavenRepositoryClientTest.java23
-rw-r--r--controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java837
-rw-r--r--controller-server/src/test/resources/application-packages/changed-deployment-xml.zipbin1476 -> 0 bytes
-rw-r--r--controller-server/src/test/resources/application-packages/changed-services-xml.zipbin1446 -> 0 bytes
-rw-r--r--controller-server/src/test/resources/application-packages/include-absolute.zipbin1488 -> 0 bytes
-rw-r--r--controller-server/src/test/resources/application-packages/include-parent.zipbin1490 -> 0 bytes
-rw-r--r--controller-server/src/test/resources/application-packages/original.zipbin1448 -> 0 bytes
-rw-r--r--controller-server/src/test/resources/application-packages/similar-deployment-xml.zipbin1494 -> 0 bytes
-rw-r--r--controller-server/src/test/resources/application-packages/with-certificate.zipbin1640 -> 0 bytes
-rw-r--r--controller-server/src/test/resources/config-models/cd/config-models-cd.xml26
-rw-r--r--controller-server/src/test/resources/config-models/cd/config-models-main.xml16
-rw-r--r--controller-server/src/test/resources/config-models/empty/config-models-cd.xml0
-rw-r--r--controller-server/src/test/resources/config-models/empty/config-models-main.xml0
-rw-r--r--controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.json37
-rw-r--r--controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.operator.json32
-rw-r--r--controller-server/src/test/resources/horizon/filter-in-execution-graph.json21
-rw-r--r--controller-server/src/test/resources/horizon/filters-complex.expected.json56
-rw-r--r--controller-server/src/test/resources/horizon/filters-complex.json46
-rw-r--r--controller-server/src/test/resources/horizon/filters-meta-query.expected.json39
-rw-r--r--controller-server/src/test/resources/horizon/filters-meta-query.json29
-rw-r--r--controller-server/src/test/resources/horizon/no-filters.expected.json32
-rw-r--r--controller-server/src/test/resources/horizon/no-filters.json16
-rw-r--r--controller-server/src/test/resources/job.json9
-rw-r--r--controller-server/src/test/resources/mail/notification.html649
-rw-r--r--controller-server/src/test/resources/mail/trial-expired.html646
-rw-r--r--controller-server/src/test/resources/mail/trial-expires-immediately.html646
-rw-r--r--controller-server/src/test/resources/mail/trial-midway-checkin.html646
-rw-r--r--controller-server/src/test/resources/mail/trial-signed-up.html646
-rw-r--r--controller-server/src/test/resources/system-flags/existing-prod.us-east-3.json8
-rw-r--r--controller-server/src/test/resources/system-flags/existing-prod.us-west-1.json8
-rw-r--r--controller-server/src/test/resources/system-flags/flags/my-flag/main.json8
-rw-r--r--controller-server/src/test/resources/system-flags/flags/my-flag/main.prod.us-east-3.json8
-rw-r--r--controller-server/src/test/resources/system-flags/partial/default.json20
-rw-r--r--controller-server/src/test/resources/system-flags/partial/initial.json15
-rw-r--r--controller-server/src/test/resources/system-flags/partial/put-controller.json15
-rw-r--r--controller-server/src/test/resources/testConfig.json24
-rw-r--r--controller-server/src/test/resources/test_runner_services.xml-cd39
-rw-r--r--controller-server/src/test/resources/test_runner_services_with_legacy_tests.xml-cd40
-rw-r--r--pom.xml2
-rw-r--r--screwdriver.yaml2
-rw-r--r--vespa-dependencies-enforcer/allowed-maven-dependencies.txt7
1008 files changed, 1 insertions, 113037 deletions
diff --git a/controller-api/OWNERS b/controller-api/OWNERS
deleted file mode 100644
index c7b017cd739..00000000000
--- a/controller-api/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-mpolden
-jonmv
diff --git a/controller-api/README b/controller-api/README
deleted file mode 100644
index ea52213b4e7..00000000000
--- a/controller-api/README
+++ /dev/null
@@ -1 +0,0 @@
-controller-api contains APIs (and mock implementations) for services used by controller-server.
diff --git a/controller-api/pom.xml b/controller-api/pom.xml
deleted file mode 100644
index 1d53302a707..00000000000
--- a/controller-api/pom.xml
+++ /dev/null
@@ -1,147 +0,0 @@
-<?xml version="1.0"?>
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
- http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>parent</artifactId>
- <version>8-SNAPSHOT</version>
- <relativePath>../parent/pom.xml</relativePath>
- </parent>
- <artifactId>controller-api</artifactId>
- <packaging>container-plugin</packaging>
- <version>8-SNAPSHOT</version>
-
- <dependencies>
-
- <!-- provided -->
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>container-dev</artifactId>
- <scope>provided</scope>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>container-apache-http-client-bundle</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>config-provisioning</artifactId>
- <scope>provided</scope>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>vespa-athenz</artifactId>
- <scope>provided</scope>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>flags</artifactId>
- <scope>provided</scope>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>vespajlib</artifactId>
- <scope>provided</scope>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>jdisc-security-filters</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>security-utils</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <!-- compile -->
-
- <dependency>
- <artifactId>aws-java-sdk-core</artifactId>
- <groupId>com.amazonaws</groupId>
- <exclusions>
- <exclusion>
- <groupId>org.apache.httpcomponents</groupId>
- <artifactId>httpclient</artifactId>
- </exclusion>
- <exclusion>
- <groupId>com.fasterxml.jackson.dataformat</groupId>
- <artifactId>*</artifactId>
- </exclusion>
- <exclusion>
- <groupId>com.fasterxml.jackson.core</groupId>
- <artifactId>*</artifactId>
- </exclusion>
- <exclusion>
- <groupId>commons-logging</groupId>
- <artifactId>*</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <!-- test -->
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>configdefinitions</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter-api</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter-engine</artifactId>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-core</artifactId>
- <scope>test</scope>
- </dependency>
-
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- </plugin>
- <plugin>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>bundle-plugin</artifactId>
- <extensions>true</extensions>
- <configuration>
- <attachBundleArtifact>true</attachBundleArtifact>
- <bundleClassifierName>deploy</bundleClassifierName>
- <useCommonAssemblyIds>false</useCommonAssemblyIds>
- </configuration>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/ApplicationApi.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/ApplicationApi.java
deleted file mode 100644
index b279af5265d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/ApplicationApi.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4;
-
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.TenantInfo;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-import java.util.List;
-
-/**
- * @author gv
- */
-@Path("/v4/")
-@Consumes(MediaType.APPLICATION_JSON)
-@Produces(MediaType.APPLICATION_JSON)
-public interface ApplicationApi {
-
- @GET
- @Path(TenantResource.API_PATH)
- List<TenantInfo> listTenants();
-
- @Path(TenantResource.API_PATH + "/{tenantId}")
- TenantResource tenant(@PathParam("tenantId")TenantId tenantId);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/ApplicationResource.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/ApplicationResource.java
deleted file mode 100644
index e8a8fb36e44..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/ApplicationResource.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.ApplicationReference;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.InstancesReply;
-
-import javax.ws.rs.DELETE;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-import java.util.List;
-
-/**
- * @author gv
- */
-@Path("") //Ensures that the produces annotation is inherited
-@Produces(MediaType.APPLICATION_JSON)
-public interface ApplicationResource {
-
- String API_PATH = "application";
-
- @GET
- List<ApplicationReference> listApplications();
-
- @Path("{applicationId}")
- @POST
- ApplicationReference createApplication(@PathParam("applicationId") ApplicationId applicationId);
-
- @Path("{applicationId}")
- @DELETE
- void deleteApplication(@PathParam("applicationId") ApplicationId applicationId);
-
- @Path("{applicationId}/environment")
- EnvironmentResource environment();
-
- @Path("{applicationId}")
- @GET
- InstancesReply listInstances(@PathParam("applicationId") ApplicationId applicationId);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/CostResource.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/CostResource.java
deleted file mode 100644
index a14edbd28a4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/CostResource.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4;
-
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.CostMonths;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.CostResult;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-
-/**
- * @author ogronnesby
- */
-@Path("") //Ensures that the produces annotation is inherited
-@Produces(MediaType.APPLICATION_JSON)
-@Consumes(MediaType.APPLICATION_JSON)
-public interface CostResource {
- String API_PATH = "cost";
-
- @GET
- @Path("/")
- CostMonths costMonths();
-
- @GET
- @Path("/{month}")
- CostResult cost(@PathParam("month") String month);
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/EnvironmentResource.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/EnvironmentResource.java
deleted file mode 100644
index 6e115667e4c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/EnvironmentResource.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.InstanceInformation;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.EnvironmentId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.InstanceId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RegionId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-
-/**
- * @author Tony Vaagenes
- * @author gv
- */
-@Path("") //Ensures that the produces annotation is inherited
-@Produces(MediaType.APPLICATION_JSON)
-@Consumes(MediaType.APPLICATION_JSON)
-public interface EnvironmentResource {
-
- String API_PATH = "environment";
-
- String APPLICATION_TEST_ZIP = "applicationTestZip";
- String APPLICATION_ZIP = "applicationZip";
- String SUBMIT_OPTIONS = "submitOptions";
-
- @DELETE
- @Path("{environmentId}/region/{regionId}/instance/{instanceId}")
- String deactivate(@PathParam("tenantId") TenantId tenantId,
- @PathParam("applicationId") ApplicationId applicationId,
- @PathParam("environmentId") EnvironmentId environmentId,
- @PathParam("regionId") RegionId regionId,
- @PathParam("instanceId") InstanceId instanceId);
-
- @POST
- @Path("{environmentId}/region/{regionId}/instance/{instanceId}/restart")
- String restart(@PathParam("tenantId") TenantId tenantId,
- @PathParam("applicationId") ApplicationId applicationId,
- @PathParam("environmentId") EnvironmentId environmentId,
- @PathParam("regionId") RegionId regionId,
- @PathParam("instanceId") InstanceId instanceId,
- @QueryParam("hostname") HostName hostname);
-
- @GET
- @Path("{environmentId}/region/{regionId}/instance/{instanceId}")
- InstanceInformation instanceInfo(@PathParam("tenantId") TenantId tenantId,
- @PathParam("applicationId") ApplicationId applicationId,
- @PathParam("environmentId") EnvironmentId environmentId,
- @PathParam("regionId") RegionId regionId,
- @PathParam("instanceId") InstanceId instanceId);
-
- @GET
- @Path("{environmentId}/region/{regionId}/instance/{instanceId}/converge")
- JsonNode waitForConfigConverge(@PathParam("tenantId") TenantId tenantId,
- @PathParam("applicationId") ApplicationId applicationId,
- @PathParam("environmentId") EnvironmentId environmentId,
- @PathParam("regionId") RegionId regionId,
- @PathParam("instanceId") InstanceId instanceId,
- @QueryParam("timeout") long timeoutInSeconds);
-
- @PUT
- @Path("{environmentId}/region/{regionId}/instance/{instanceId}/global-rotation/override")
- String setRotationOut(@PathParam("tenantId") TenantId tenantId,
- @PathParam("applicationId") ApplicationId applicationId,
- @PathParam("environmentId") EnvironmentId environmentId,
- @PathParam("regionId") RegionId regionId,
- @PathParam("instanceId") InstanceId instanceId);
-
- @DELETE
- @Path("{environmentId}/region/{regionId}/instance/{instanceId}/global-rotation/override")
- String setRotationIn(@PathParam("tenantId") TenantId tenantId,
- @PathParam("applicationId") ApplicationId applicationId,
- @PathParam("environmentId") EnvironmentId environmentId,
- @PathParam("regionId") RegionId regionId,
- @PathParam("instanceId") InstanceId instanceId);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/TenantResource.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/TenantResource.java
deleted file mode 100644
index 541110f2662..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/TenantResource.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4;
-
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.TenantCreateOptions;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.TenantInfo;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.TenantUpdateOptions;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.TenantWithApplications;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-
-/**
- * @author Tony Vaagenes
- */
-@Path("") //Ensures that the produces annotation is inherited
-@Produces(MediaType.APPLICATION_JSON)
-@Consumes(MediaType.APPLICATION_JSON)
-public interface TenantResource {
-
- String API_PATH = "tenant";
-
- @GET
- TenantWithApplications metaData();
-
- @DELETE
- TenantInfo deleteTenant();
-
- @POST
- TenantInfo createTenant(TenantCreateOptions tenantOptions);
-
- @PUT
- TenantInfo updateTenant(TenantUpdateOptions tenantOptions);
-
- @Path(ApplicationResource.API_PATH)
- ApplicationResource application();
-
- @Path(CostResource.API_PATH)
- CostResource cost();
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/ApplicationReference.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/ApplicationReference.java
deleted file mode 100644
index 2001dbf7cd9..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/ApplicationReference.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
-
-import java.net.URI;
-
-/**
- * @author Stian Kristoffersen
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class ApplicationReference {
- public ApplicationId application;
- public URI url;
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/ClusterMetrics.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/ClusterMetrics.java
deleted file mode 100644
index 59e6c4d647f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/ClusterMetrics.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * @author olaa
- */
-public class ClusterMetrics {
-
- // These field names originate from the DeploymentMetricsResponse class
- public static final String QUERIES_PER_SECOND = "queriesPerSecond";
- public static final String FEED_PER_SECOND = "feedPerSecond";
- public static final String DOCUMENT_COUNT = "documentCount";
- public static final String FEED_LATENCY = "feedLatency";
- public static final String QUERY_LATENCY = "queryLatency";
- public static final String MEMORY_UTIL = "memoryUtil";
- public static final String MEMORY_FEED_BLOCK_LIMIT = "memoryFeedBlockLimit";
- public static final String DISK_UTIL = "diskUtil";
- public static final String DISK_FEED_BLOCK_LIMIT = "diskFeedBlockLimit";
-
- private final String clusterId;
- private final String clusterType;
- private final Map<String, Double> metrics;
-
- public ClusterMetrics(String clusterId, String clusterType, Map<String, Double> metrics) {
- this.clusterId = clusterId;
- this.clusterType = clusterType;
- this.metrics = Map.copyOf(metrics);
- }
-
- public String getClusterId() {
- return clusterId;
- }
-
- public String getClusterType() {
- return clusterType;
- }
-
- public Optional<Double> queriesPerSecond() {
- return Optional.ofNullable(metrics.get(QUERIES_PER_SECOND));
- }
-
- public Optional<Double> feedPerSecond() {
- return Optional.ofNullable(metrics.get(FEED_PER_SECOND));
- }
-
- public Optional<Double> documentCount() {
- return Optional.ofNullable(metrics.get(DOCUMENT_COUNT));
- }
-
- public Optional<Double> feedLatency() {
- return Optional.ofNullable(metrics.get(FEED_LATENCY));
- }
-
- public Optional<Double> queryLatency() {
- return Optional.ofNullable(metrics.get(QUERY_LATENCY));
- }
-
- public Optional<Double> memoryUtil() {
- return Optional.ofNullable(metrics.get(MEMORY_UTIL));
- }
-
- public Optional<Double> memoryFeedBlockLimit() {
- return Optional.ofNullable(metrics.get(MEMORY_FEED_BLOCK_LIMIT));
- }
-
- public Optional<Double> diskUtil() {
- return Optional.ofNullable(metrics.get(DISK_UTIL));
- }
-
- public Optional<Double> diskFeedBlockLimit() {
- return Optional.ofNullable(metrics.get(DISK_FEED_BLOCK_LIMIT));
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostItem.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostItem.java
deleted file mode 100644
index c8cf1b4631e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostItem.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-
-/**
- * @author ogronnesby
- */
-public class CostItem {
- private String applicationId;
- private String zoneId;
- private CostItemUsage cpu;
- private CostItemUsage memory;
- private CostItemUsage disk;
-
- public CostItem() {}
-
- public ApplicationId getApplicationId() {
- return ApplicationId.fromSerializedForm(applicationId);
- }
-
- public ZoneId getZoneId() {
- return ZoneId.from(zoneId);
- }
-
- public CostItemUsage getCpu() {
- return cpu;
- }
-
- public CostItemUsage getMemory() {
- return memory;
- }
-
- public CostItemUsage getDisk() {
- return disk;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostItemUsage.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostItemUsage.java
deleted file mode 100644
index 814b41d3dbf..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostItemUsage.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import java.math.BigDecimal;
-
-/**
- * @author ogronnesby
- */
-public class CostItemUsage {
- private BigDecimal usage;
- private BigDecimal charge;
-
- public CostItemUsage() {}
-
- public BigDecimal getUsage() {
- return usage;
- }
-
- public BigDecimal getCharge() {
- return charge;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostMonths.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostMonths.java
deleted file mode 100644
index 8fbdaffc432..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostMonths.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import java.util.List;
-
-/**
- * @author ogronnesby
- */
-public class CostMonths {
- public List<String> months;
-
- public List<String> getMonths() {
- return months;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostResult.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostResult.java
deleted file mode 100644
index 037ef92e030..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/CostResult.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import java.util.List;
-
-/**
- * @author ogronnesby
- */
-public class CostResult {
- private String month;
- private List<CostItem> items;
-
- public CostResult() {}
-
- public String getMonth() {
- return month;
- }
-
- public List<CostItem> getItems() {
- return items;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java
deleted file mode 100644
index fd4a34118c5..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentData.java
+++ /dev/null
@@ -1,164 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.DockerImage;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.yolean.concurrent.Memoized;
-
-import java.io.InputStream;
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Supplier;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Data pertaining to a deployment to be done on a config server.
- * Accessor names must match the names in com.yahoo.vespa.config.server.session.PrepareParams.
- *
- * @author jonmv
- */
-public class DeploymentData {
-
- private static final Logger log = Logger.getLogger(DeploymentData.class.getName());
-
- private final ApplicationId instance;
- private final ZoneId zone;
- private final Supplier<InputStream> applicationPackage;
- private final Version platform;
- private final Supplier<DeploymentEndpoints> endpoints;
- private final Optional<DockerImage> dockerImageRepo;
- private final Optional<AthenzDomain> athenzDomain;
- private final Supplier<Quota> quota;
- private final List<TenantSecretStore> tenantSecretStores;
- private final List<X509Certificate> operatorCertificates;
- private final Supplier<Optional<CloudAccount>> cloudAccount;
- private final Supplier<List<DataplaneTokenVersions>> dataPlaneTokens;
- private final boolean dryRun;
-
- public DeploymentData(ApplicationId instance, ZoneId zone, Supplier<InputStream> applicationPackage, Version platform,
- Supplier<DeploymentEndpoints> endpoints,
- Optional<DockerImage> dockerImageRepo,
- Optional<AthenzDomain> athenzDomain,
- Supplier<Quota> quota,
- List<TenantSecretStore> tenantSecretStores,
- List<X509Certificate> operatorCertificates,
- Supplier<Optional<CloudAccount>> cloudAccount,
- Supplier<List<DataplaneTokenVersions>> dataPlaneTokens,
- boolean dryRun) {
- this.instance = requireNonNull(instance);
- this.zone = requireNonNull(zone);
- this.applicationPackage = requireNonNull(applicationPackage);
- this.platform = requireNonNull(platform);
- this.endpoints = wrap(requireNonNull(endpoints), Duration.ofSeconds(30), "deployment endpoints for " + instance + " in " + zone);
- this.dockerImageRepo = requireNonNull(dockerImageRepo);
- this.athenzDomain = athenzDomain;
- this.quota = wrap(requireNonNull(quota), Duration.ofSeconds(10), "quota for " + instance);
- this.tenantSecretStores = List.copyOf(requireNonNull(tenantSecretStores));
- this.operatorCertificates = List.copyOf(requireNonNull(operatorCertificates));
- this.cloudAccount = wrap(requireNonNull(cloudAccount), Duration.ofSeconds(5), "cloud account for " + instance + " in " + zone);
- this.dataPlaneTokens = wrap(dataPlaneTokens, Duration.ofSeconds(5), "data plane tokens for " + instance + " in " + zone);
- this.dryRun = dryRun;
- }
-
- public ApplicationId instance() {
- return instance;
- }
-
- public ZoneId zone() {
- return zone;
- }
-
- public InputStream applicationPackage() {
- return applicationPackage.get();
- }
-
- public Version platform() {
- return platform;
- }
-
- public DeploymentEndpoints endpoints() {
- return endpoints.get();
- }
-
- public Optional<DockerImage> dockerImageRepo() {
- return dockerImageRepo;
- }
-
- public Optional<AthenzDomain> athenzDomain() {
- return athenzDomain;
- }
-
- public Quota quota() {
- return quota.get();
- }
-
- public List<TenantSecretStore> tenantSecretStores() {
- return tenantSecretStores;
- }
-
- public List<X509Certificate> operatorCertificates() {
- return operatorCertificates;
- }
-
- public Optional<CloudAccount> cloudAccount() {
- return cloudAccount.get();
- }
-
- public List<DataplaneTokenVersions> dataPlaneTokens() {
- return dataPlaneTokens.get();
- }
-
- public boolean isDryRun() {
- return dryRun;
- }
-
- private static <T> Supplier<T> wrap(Supplier<T> delegate, Duration timeout, String description) {
- return new TimingSupplier<>(new Memoized<>(delegate), timeout, description);
- }
-
- public static class TimingSupplier<T> implements Supplier<T> {
-
- private final Supplier<T> delegate;
- private final Duration timeout;
- private final String description;
-
- public TimingSupplier(Supplier<T> delegate, Duration timeout, String description) {
- this.delegate = delegate;
- this.timeout = timeout;
- this.description = description;
- }
-
- @Override
- public T get() {
- long startNanos = System.nanoTime();
- Throwable thrown = null;
- try {
- return delegate.get();
- }
- catch (Throwable t) {
- thrown = t;
- throw t;
- }
- finally {
- long durationNanos = System.nanoTime() - startNanos;
- Level level = durationNanos > timeout.toNanos() ? Level.WARNING : Level.FINE;
- String thrownMessage = thrown == null ? "" : " with exception " + thrown;
- log.log(level, () -> String.format("Getting %s took %.6f seconds%s", description, durationNanos / 1e9, thrownMessage));
- }
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentEndpoints.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentEndpoints.java
deleted file mode 100644
index 82172c18b6c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentEndpoints.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerEndpoint;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * The endpoints and their certificate (if any) of a deployment.
- *
- * @author mpolden
- */
-public record DeploymentEndpoints(Set<ContainerEndpoint> endpoints, Optional<EndpointCertificate> certificate) {
-
- public static final DeploymentEndpoints none = new DeploymentEndpoints(Set.of(), Optional.empty());
-
- public DeploymentEndpoints {
- Objects.requireNonNull(endpoints);
- Objects.requireNonNull(certificate);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentReference.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentReference.java
deleted file mode 100644
index e7f47781d6f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/DeploymentReference.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.yahoo.vespa.hosted.controller.api.identifiers.EnvironmentId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.RegionId;
-
-import java.net.URI;
-
-/**
- * @author jonmv
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class DeploymentReference {
- public EnvironmentId environment;
- public RegionId region;
-
- public URI url;
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/EndpointStatus.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/EndpointStatus.java
deleted file mode 100644
index 438ce1351d1..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/EndpointStatus.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import java.time.Instant;
-import java.util.Objects;
-
-/**
- * Represent the routing status for all endpoints of a deployment.
- *
- * @author smorgrav
- */
-public class EndpointStatus {
-
- private final String agent;
- private final Status status;
- private final Instant changedAt;
-
- public EndpointStatus(Status status, String agent, Instant changedAt) {
- this.status = Objects.requireNonNull(status);
- this.agent = Objects.requireNonNull(agent);
- this.changedAt = Objects.requireNonNull(changedAt);
- }
-
- /** Returns the agent responsible setting this status */
- public String agent() {
- return agent;
- }
-
- /** Returns the current status */
- public Status status() {
- return status;
- }
-
- /** Returns when this was last changed */
- public Instant changedAt() {
- return changedAt;
- }
-
- public enum Status {
- in,
- out,
- unknown;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstanceInformation.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstanceInformation.java
deleted file mode 100644
index 2faaff12277..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstanceInformation.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.config.provision.zone.RoutingMethod;
-
-import java.net.URI;
-import java.util.List;
-
-/**
- * @author mortent
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class InstanceInformation {
-
- public List<Endpoint> endpoints;
- public URI yamasUrl;
- public Long deployTimeEpochMs;
- public Long expiryTimeEpochMs;
-
- public static class Endpoint {
- public String cluster;
- public boolean tls;
- public URI url;
- public String scope;
- public RoutingMethod routingMethod;
- public String auth;
-
- @JsonCreator
- public Endpoint(@JsonProperty("cluster") String cluster ,
- @JsonProperty("tls") boolean tls,
- @JsonProperty("url") URI url,
- @JsonProperty("scope") String scope,
- @JsonProperty("routingMethod") RoutingMethod routingMethod,
- @JsonProperty("authMethod") String auth) {
- this.cluster = cluster;
- this.tls = tls;
- this.url = url;
- this.scope = scope;
- this.routingMethod = routingMethod;
- this.auth = auth;
- }
-
- @Override
- public String toString() {
- return "Endpoint{" +
- "cluster='" + cluster + '\'' +
- ", tls=" + tls +
- ", url=" + url +
- ", scope='" + scope + '\'' +
- ", authType='" + auth + '\'' +
- ", routingMethod=" + routingMethod +
- '}';
- }
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstanceReference.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstanceReference.java
deleted file mode 100644
index 1490ff2c256..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstanceReference.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.yahoo.vespa.hosted.controller.api.identifiers.InstanceId;
-
-import java.net.URI;
-import java.util.List;
-import java.util.Set;
-
-/**
- * @author mortent
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class InstanceReference {
-
- public List<DeploymentReference> deployments;
- public InstanceId instance;
- public Set<URI> globalRotations;
- public String rotationId;
- public URI url;
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstancesReply.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstancesReply.java
deleted file mode 100644
index b831f2c191d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/InstancesReply.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-
-import java.util.List;
-
-/**
- * @author Tony Vaagenes
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class InstancesReply {
- public List<InstanceReference> instances;
- public String compileVersion;
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/JsonResponse.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/JsonResponse.java
deleted file mode 100644
index 3f453e310aa..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/JsonResponse.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-
-/**
- * @author gv
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(Include.NON_NULL)
-public abstract class JsonResponse<DATA> {
- public DATA data;
-
- public JsonResponse(DATA data) {
- this.data = data;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/LogEntry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/LogEntry.java
deleted file mode 100644
index c22e367be3f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/LogEntry.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * @author gjoranv
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class LogEntry {
-
- public final long time;
- public final String level;
- public final String message;
-
- @JsonCreator
- public LogEntry(@JsonProperty("time") long time,
- @JsonProperty("level") String level,
- @JsonProperty("message") String message) {
- this.time = time;
- this.level = level;
- this.message = message;
- }
-
- @Override
- public String toString() {
- return "LogEntry{" +
- "time=" + time +
- ", level='" + level + '\'' +
- ", message='" + message + '\'' +
- '}';
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/SearchNodeMetrics.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/SearchNodeMetrics.java
deleted file mode 100644
index 71eaa432773..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/SearchNodeMetrics.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-public class SearchNodeMetrics {
-
- private static final ObjectMapper jsonMapper = new ObjectMapper();
-
- private static final Logger logger = Logger.getLogger(SearchNodeMetrics.class.getName());
-
- public static final String DOCUMENTS_ACTIVE_COUNT = "documentsActiveCount";
- public static final String DOCUMENTS_READY_COUNT = "documentsReadyCount";
- public static final String DOCUMENTS_TOTAL_COUNT = "documentsTotalCount";
- public static final String DOCUMENT_DISK_USAGE = "documentDiskUsage";
- public static final String RESOURCE_DISK_USAGE_AVERAGE = "resourceDiskUsageAverage";
- public static final String RESOURCE_MEMORY_USAGE_AVERAGE = "resourceMemoryUsageAverage";
-
- private final String clusterId;
- private final Map<String, Double> metrics;
-
- public SearchNodeMetrics(String clusterId) {
- this.clusterId = clusterId;
- metrics = new HashMap<>();
- }
-
- public String getClusterId() { return clusterId; }
-
- public double documentsActiveCount() { return metrics.get(DOCUMENTS_ACTIVE_COUNT); }
-
- public double documentsReadyCount() { return metrics.get(DOCUMENTS_READY_COUNT); }
-
- public double documentsTotalCount() { return metrics.get(DOCUMENTS_TOTAL_COUNT); }
-
- public double documentDiskUsage() { return metrics.get(DOCUMENT_DISK_USAGE); }
-
- public double resourceDiskUsageAverage() { return metrics.get(RESOURCE_DISK_USAGE_AVERAGE); }
-
- public double resourceMemoryUsageAverage() { return metrics.get(RESOURCE_MEMORY_USAGE_AVERAGE); }
-
- public SearchNodeMetrics addMetric(String name, double value) {
- metrics.put(name, value);
- return this;
- }
-
- public JsonNode toJson() {
- try {
- ObjectNode protonMetrics = jsonMapper.createObjectNode();
- protonMetrics.put("clusterId", clusterId);
- ObjectNode jsonMetrics = jsonMapper.createObjectNode();
- for (Map.Entry<String, Double> entry : metrics.entrySet()) {
- jsonMetrics.put(entry.getKey(), entry.getValue());
- }
- protonMetrics.set("metrics", jsonMetrics);
- return protonMetrics;
- } catch (Exception e) {
- logger.log(Level.SEVERE, "Unable to convert Proton Metrics to JSON Object: " + e.getMessage(), e);
- }
- return jsonMapper.createObjectNode();
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantCreateOptions.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantCreateOptions.java
deleted file mode 100644
index b2ee8f0cf5c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantCreateOptions.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-
-/**
- * @author bjorncs
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(value = JsonInclude.Include.NON_NULL)
-public class TenantCreateOptions {
- public AthenzDomain athensDomain;
- public Property property;
- public PropertyId propertyId;
-
- public TenantCreateOptions() {}
-
- public TenantCreateOptions(AthenzDomain athensDomain, Property property, PropertyId propertyId) {
- this.athensDomain = athensDomain;
- this.property = property;
- this.propertyId = propertyId;
- }
-
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("options: ");
- sb.append("athens-domain='").append(this.athensDomain.getName()).append("', ");
- sb.append("property='").append(this.property).append("'");
- if (this.propertyId != null) {
- sb.append(", propertyId='").append(this.propertyId).append("'");
- }
-
- return sb.toString();
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantInfo.java
deleted file mode 100644
index 94ae6e6bab1..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantInfo.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
-
-import java.net.URI;
-
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(value = JsonInclude.Include.NON_EMPTY)
-public class TenantInfo {
- public TenantId tenant;
- // TODO: make optional
- public TenantMetaData metaData;
- public URI url;
-
- // Required for Jackson deserialization
- public TenantInfo() {}
-
- public TenantInfo(TenantId tenantId, TenantMetaData metaData, URI url) {
- this.tenant = tenantId;
- this.metaData = metaData;
- this.url = url;
- }
-
- public TenantInfo(TenantId tenant, URI url) {
- this.tenant = tenant;
- this.url = url;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantMetaData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantMetaData.java
deleted file mode 100644
index e87fd7dc4cf..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantMetaData.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonInclude.Include;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-
-import java.util.Optional;
-
-/**
- * @author gv
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(value = Include.NON_EMPTY)
-public class TenantMetaData {
- public TenantType type;
- public Optional<AthenzDomain> athensDomain;
- public Optional<Property> property;
-
- // Required for Jackson deserialization
- public TenantMetaData() {}
-
- public TenantMetaData(TenantType type,
- Optional<AthenzDomain> athensDomain,
- Optional<Property> property) {
- this.type = type;
- this.athensDomain = athensDomain;
- this.property = property;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantType.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantType.java
deleted file mode 100644
index 292fa97d83d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantType.java
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-/**
- * @author bjorncs
- */
-public enum TenantType {
- USER,
- ATHENS,
- CLOUD
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantUpdateOptions.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantUpdateOptions.java
deleted file mode 100644
index 7abd6e273c9..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantUpdateOptions.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * @author gv
- * @author bjorncs
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(value = JsonInclude.Include.NON_ABSENT)
-public class TenantUpdateOptions {
- public final Property property;
- public final Optional<AthenzDomain> athensDomain;
-
- @JsonCreator
- public TenantUpdateOptions(@JsonProperty("property") Property property,
- @JsonProperty("athensDomain") Optional<AthenzDomain> athensDomain) {
- this.property = property;
- this.athensDomain = athensDomain;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TenantUpdateOptions that = (TenantUpdateOptions) o;
- return Objects.equals(property, that.property) &&
- Objects.equals(athensDomain, that.athensDomain);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(property, athensDomain);
- }
-
- @Override
- public String toString() {
- return "TenantUpdateOptions{" +
- "property=" + property +
- ", athensDomain=" + athensDomain +
- '}';
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantWithApplications.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantWithApplications.java
deleted file mode 100644
index a4f87e5e0e1..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/TenantWithApplications.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-
-import java.util.List;
-
-/**
- * @author Tony Vaagenes
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(value = JsonInclude.Include.NON_NULL)
-public class TenantWithApplications {
- // TODO: use TenantMetaData instead of individual fields (requires dashboard updates)
- public TenantType type;
- public AthenzDomain athensDomain;
- public Property property;
- public List<ApplicationReference> applications;
-
- public TenantWithApplications() {}
-
- public TenantWithApplications(
- TenantType type,
- AthenzDomain athensDomain,
- Property property,
- List<ApplicationReference> applications) {
- this.type = type;
- this.athensDomain = athensDomain;
- this.property = property;
- this.applications = applications;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/package-info.java
deleted file mode 100644
index afaf05cf0ed..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/model/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.application.v4.model;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/package-info.java
deleted file mode 100644
index d50646e8e47..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/application/v4/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.application.v4;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/Environment.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/Environment.java
deleted file mode 100644
index d191a9b8f82..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/Environment.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.configserver;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonValue;
-
-/**
- * Environment representation using the same definition as configserver. And allowing
- * serialization/deserialization to/from JSON.
- *
- * @author Ulf Lilleengen
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class Environment {
- private final com.yahoo.config.provision.Environment environment;
-
- public Environment(com.yahoo.config.provision.Environment environment) {
- this.environment = environment;
- }
-
- @JsonValue
- public String value() {
- return environment.value();
- }
-
- @Override
- public String toString() {
- return value();
- }
-
- public com.yahoo.config.provision.Environment getEnvironment() {
- return environment;
- }
-
- public Environment(String environment) {
- this.environment = com.yahoo.config.provision.Environment.from(environment);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/Region.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/Region.java
deleted file mode 100644
index 9114971e2d4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/Region.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.configserver;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonValue;
-import com.yahoo.config.provision.RegionName;
-
-/**
- * Region representation using the same definition as configserver. And allowing
- * serialization/deserialization to/from JSON.
- *
- * @author Ulf Lilleengen
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class Region {
- private final RegionName region;
-
- public Region(RegionName region) {
- this.region = region;
- }
-
- @JsonValue
- public String value() {
- return region.value();
- }
-
- @Override
- public String toString() { return value(); }
-
- public RegionName getRegion() {
- return region;
- }
-
- @JsonCreator
- public Region(String region) {
- this.region = com.yahoo.config.provision.RegionName.from(region);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/package-info.java
deleted file mode 100644
index 31406a2dc5b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/configserver/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.configserver;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ApplicationId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ApplicationId.java
deleted file mode 100644
index 96bbff56242..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ApplicationId.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author smorgrav
- */
-public class ApplicationId extends NonDefaultIdentifier {
-
- public ApplicationId(String id) {
- super(id);
- }
-
- @Override
- public void validate() {
- super.validate();
- validateNoUpperCase();
- }
-
- public static void validate(String id) {
- if ( ! strictPattern.matcher(id).matches())
- throwInvalidId(id, strictPatternExplanation);
- new ApplicationId(id); // validate
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ClusterId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ClusterId.java
deleted file mode 100644
index 7cec4068c2e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ClusterId.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-import com.yahoo.config.provision.ClusterSpec;
-
-import java.util.Objects;
-
-/**
- * DeploymentId x ClusterSpec.Id = ClusterId
- *
- * @author ogronnesby
- */
-public class ClusterId {
- private final DeploymentId deploymentId;
- private final ClusterSpec.Id clusterId;
-
- public ClusterId(DeploymentId deploymentId, ClusterSpec.Id clusterId) {
- this.deploymentId = deploymentId;
- this.clusterId = clusterId;
- }
-
- public DeploymentId deploymentId() {
- return deploymentId;
- }
-
- public ClusterSpec.Id clusterId() {
- return clusterId;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ClusterId clusterId1 = (ClusterId) o;
- return Objects.equals(deploymentId, clusterId1.deploymentId) && Objects.equals(clusterId, clusterId1.clusterId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(deploymentId, clusterId);
- }
-
- @Override
- public String toString() {
- return "ClusterId{" +
- "deploymentId=" + deploymentId +
- ", clusterId=" + clusterId +
- '}';
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ControllerVersion.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ControllerVersion.java
deleted file mode 100644
index 0f6cf6bb78d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ControllerVersion.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-import com.yahoo.component.Version;
-import com.yahoo.component.Vtag;
-
-import java.time.Instant;
-import java.util.Objects;
-
-/**
- * A controller's Vespa version and commit details.
- *
- * @author mpolden
- */
-public class ControllerVersion implements Comparable<ControllerVersion> {
-
- /** The current version of this controller */
- public static final ControllerVersion CURRENT = new ControllerVersion(Vtag.currentVersion, Vtag.commitSha, Vtag.commitDate);
-
- private final Version version;
- private final String commitSha;
- private final Instant commitDate;
-
- public ControllerVersion(Version version, String commitSha, Instant commitDate) {
- this.version = Objects.requireNonNull(version);
- this.commitSha = Objects.requireNonNull(commitSha);
- this.commitDate = Objects.requireNonNull(commitDate);
- }
-
- /** Vespa version */
- public Version version() {
- return version;
- }
-
- /** Commit SHA of this */
- public String commitSha() {
- return commitSha;
- }
-
- /** The time this was committed */
- public Instant commitDate() {
- return commitDate;
- }
-
- @Override
- public String toString() {
- return version + ", commit " + commitSha + " @ " + commitDate;
- }
-
- @Override
- public int compareTo(ControllerVersion o) {
- return version.compareTo(o.version);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ControllerVersion that = (ControllerVersion) o;
- return version.equals(that.version);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(version);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/DeploymentId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/DeploymentId.java
deleted file mode 100644
index 58b7e4f38ff..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/DeploymentId.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.Objects;
-
-/**
- * ApplicationId x ZoneId.
- *
- * @author smorgrav
- * @author bratseth
- */
-public class DeploymentId {
-
- private final com.yahoo.config.provision.ApplicationId applicationId;
- private final ZoneId zoneId;
-
- public DeploymentId(com.yahoo.config.provision.ApplicationId applicationId, ZoneId zoneId) {
- this.applicationId = applicationId;
- this.zoneId = zoneId;
- }
-
- public com.yahoo.config.provision.ApplicationId applicationId() {
- return applicationId;
- }
-
- public ZoneId zoneId() {
- return zoneId;
- }
-
- public String dottedString() {
- return unCapitalize(applicationId().tenant().value()) + "."
- + unCapitalize(applicationId().application().value()) + "."
- + unCapitalize(zoneId.environment().value()) + "."
- + unCapitalize(zoneId.region().value()) + "."
- + unCapitalize(applicationId.instance().value());
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if ( ! (o instanceof DeploymentId)) return false;
- DeploymentId id = (DeploymentId) o;
- return Objects.equals(applicationId, id.applicationId) &&
- Objects.equals(zoneId, id.zoneId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(applicationId, zoneId);
- }
-
- @Override
- public String toString() {
- return toUserFriendlyString();
- }
-
- public String toUserFriendlyString() {
- return applicationId + " in " + zoneId;
- }
-
- private static String unCapitalize(String str) {
- return str.toLowerCase().substring(0,1) + str.substring(1);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/EnvironmentId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/EnvironmentId.java
deleted file mode 100644
index 2d95ad9380b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/EnvironmentId.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author smorgrav
- */
-public class EnvironmentId extends NonDefaultIdentifier {
-
- public EnvironmentId(String id) {
- super(id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitBranch.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitBranch.java
deleted file mode 100644
index ebc20d372df..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitBranch.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author smorgrav
- */
-public class GitBranch extends Identifier {
-
- public GitBranch(String id) {
- super(id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitCommit.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitCommit.java
deleted file mode 100644
index 23d5acaae37..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitCommit.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author smorgrav
- */
-public class GitCommit extends Identifier {
-
- public GitCommit(String id) {
- super(id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitRepository.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitRepository.java
deleted file mode 100644
index 7b97044af12..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/GitRepository.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author smorgrav
- */
-public class GitRepository extends Identifier {
-
- public GitRepository(String id) {
- super(id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java
deleted file mode 100644
index e8814b199ba..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Identifier.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonValue;
-
-import java.util.Objects;
-import java.util.regex.Pattern;
-
-/**
- * @author smorgrav
- */
-public abstract class Identifier {
-
- // Note: The limit of 20 characters is due to hostname being 'cluster--app--tenant', which can be max. 63 characters.
- // This is an issue for level 7 routing, with level 4 routing we do not have the same limitation
- protected static final String strictPatternExplanation =
- "Tenant, application and instance names must start with a letter, may contain no more than 20 " +
- "characters, and may only contain lowercase letters, digits or dashes, but no double-dashes.";
- protected static final Pattern strictPattern = Pattern.compile("^(?=.{1,20}$)[a-z](-?[a-z0-9]+)*$");
- private static final Pattern serializedIdentifierPattern = Pattern.compile("[a-zA-Z0-9_-]+");
- private static final Pattern serializedPattern = Pattern.compile("[a-zA-Z0-9_.-]+");
-
- private final String id;
-
- @JsonCreator
- public Identifier(String id) {
- Objects.requireNonNull(id, "Id string cannot be null");
- this.id = id;
- validate();
- }
-
- public String toDns() {
- return id.replace('_', '-');
- }
-
- @Override
- public String toString() {
- return id;
- }
-
- @JsonValue
- public String id() { return id; }
-
- public String capitalizedType() {
- String simpleName = this.getClass().getSimpleName();
- String suffix = "Id";
- if (simpleName.endsWith(suffix)) {
- simpleName = simpleName.substring(0, simpleName.length() - suffix.length());
- }
- return simpleName;
- }
-
- public void validate() {
- if (id.equals("api")) {
- throwInvalidId(id, "'api' not allowed.");
- }
- }
-
- protected void validateSerialized() {
- if (!serializedPattern.matcher(id).matches()) {
- throwInvalidId(id, "Must match pattern " + serializedPattern);
- }
- }
-
- protected void validateSerializedIdentifier() {
- if (!serializedIdentifierPattern.matcher(id).matches()) {
- throwInvalidId(id, "Must match pattern " + serializedIdentifierPattern);
- }
- }
-
- protected void validateNoDefault() {
- if (id.equals("default")) {
- throwInvalidId(id, "'default' not allowed.");
- }
- }
-
- protected void validateNoUpperCase() {
- if (!id.equals(id.toLowerCase()))
- throwInvalidId(id, "Uppercase not allowed.");
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Identifier identity = (Identifier) o;
-
- return id.equals(identity.id);
-
- }
-
- @Override
- public int hashCode() {
- return id.hashCode();
- }
-
- public static void throwInvalidId(String id, String explanation) {
- throw new IllegalArgumentException(String.format("Invalid id '%s'. %s", id, explanation));
- }
-
-}
-
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/InstanceId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/InstanceId.java
deleted file mode 100644
index 49d0af12c45..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/InstanceId.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author smorgrav
- */
-public class InstanceId extends SerializedIdentifier {
-
- public InstanceId(String id) {
- super(id);
- }
-
- @Override
- public void validate() {
- super.validate();
- validateNoUpperCase();
- }
-
- public static void validate(String id) {
- if ( ! strictPattern.matcher(id).matches())
- throwInvalidId(id, strictPatternExplanation);
- new InstanceId(id); // validate
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/MetricsType.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/MetricsType.java
deleted file mode 100644
index fd95bd6fd81..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/MetricsType.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author akvalsvik
- */
-public class MetricsType extends SerializedIdentifier {
-
- public MetricsType(String id) {
- super(id);
- }
-
- @Override
- public void validate() {
- super.validate();
- validateNoUpperCase();
- }
-
- public static void validate(String id) {
- if (!(id.equals("deployment") || id.equals("proton"))) {
- throwInvalidId(id, "MetricsType be \"deployment\" or \"proton\"");
- }
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/NonDefaultIdentifier.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/NonDefaultIdentifier.java
deleted file mode 100644
index 92a7f360462..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/NonDefaultIdentifier.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * TODO: Class description
- *
- * @author smorgrav
- */
-public abstract class NonDefaultIdentifier extends SerializedIdentifier {
-
- public NonDefaultIdentifier(String id) {
- super(id);
- }
-
- @Override
- public void validate() {
- super.validate();
- validateNoDefault();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Property.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Property.java
deleted file mode 100644
index 8733fc5181f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/Property.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * A business property.
- *
- * @author smorgrav
- */
-public class Property extends Identifier {
-
- public Property(String id) {
- super(id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/PropertyId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/PropertyId.java
deleted file mode 100644
index c9d6c2afb7e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/PropertyId.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-import java.util.regex.Pattern;
-
-/**
- * A business property ID.
- *
- * @author frodelu
- */
-public class PropertyId extends Identifier {
-
- private static final Pattern PATTERN = Pattern.compile("\\d+");
-
- public PropertyId(String id) {
- super(id);
- }
-
- /** Returns this id as a long */
- public long value() { return Long.parseLong(id()); }
-
- @Override
- public void validate() {
- super.validate();
- if(!PATTERN.matcher(id()).matches()) {
- throwInvalidId(id(), "Property id must match pattern: " + PATTERN);
- }
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RegionId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RegionId.java
deleted file mode 100644
index 5a0c54af86b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RegionId.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author smorgrav
- */
-public class RegionId extends NonDefaultIdentifier {
-
- public RegionId(String id) {
- super(id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RevisionId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RevisionId.java
deleted file mode 100644
index 663dc2b1cc6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/RevisionId.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * An unique identifier of an application package.
- *
- * @author smorgrav
- */
-public class RevisionId extends Identifier {
-
- public RevisionId(String id) {
- super(id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ScrewdriverId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ScrewdriverId.java
deleted file mode 100644
index db843e7cd66..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/ScrewdriverId.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-import java.util.regex.Pattern;
-
-/**
- * @author smorgrav
- * @author bjorncs
- */
-public class ScrewdriverId extends Identifier {
-
- // TODO: If only there was a separate type for this ...
- // This demonstrates why this subclassing scheme is a bad idea
- private static final Pattern PATTERN = Pattern.compile("\\d+");
-
- public ScrewdriverId(String id) {
- super(id);
- }
-
- /** Returns this id as a long */
- public long value() { return Long.parseLong(id()); }
-
- @Override
- public void validate() {
- super.validate();
- if(!PATTERN.matcher(id()).matches()) {
- throwInvalidId(id(), "Screwdriver id must match pattern: " + PATTERN);
- }
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/SerializedIdentifier.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/SerializedIdentifier.java
deleted file mode 100644
index 5f56888a472..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/SerializedIdentifier.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * TODO: Class description
- *
- * @author smorgrav
- */
-
-public abstract class SerializedIdentifier extends Identifier {
-
- public SerializedIdentifier(String id) {
- super(id);
- }
-
- @Override
- public void validate() {
- super.validate();
- validateSerializedIdentifier();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/TenantId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/TenantId.java
deleted file mode 100644
index 6628a4246a3..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/TenantId.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author smorgrav
- */
-public class TenantId extends NonDefaultIdentifier {
-
- public TenantId(String id) {
- super(id);
- }
-
- @Override
- public void validate() {
- super.validate();
- validateNoUpperCase();
- }
-
- public static void validate(String id) {
- if ( ! strictPattern.matcher(id).matches())
- throwInvalidId(id, strictPatternExplanation);
- new TenantId(id); // validate
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/UserGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/UserGroup.java
deleted file mode 100644
index 82fd914310c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/UserGroup.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author smorgrav
- */
-public class UserGroup extends Identifier {
-
- public UserGroup(String id) {
- super(id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/UserId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/UserId.java
deleted file mode 100644
index f2338fd135f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/UserId.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-/**
- * @author smorgrav
- */
-public class UserId extends NonDefaultIdentifier {
-
- public UserId(String id) {
- super(id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/package-info.java
deleted file mode 100644
index b140d8a2498..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/identifiers/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrls.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrls.java
deleted file mode 100644
index e741fb8d203..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrls.java
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-
-import java.net.URI;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-
-/**
- * Generates URLs to various views in the Console. Prefer to create new methods and return
- * String instead of URI to make it easier to track which views are linked from where.
- *
- * @author freva
- */
-public class ConsoleUrls {
- private final String root;
- public ConsoleUrls(URI root) {
- this.root = root.toString().replaceFirst("/$", ""); // Remove trailing slash
- }
-
- public ConsoleUrls(String hostname) { this(URI.create("https://" + hostname)); }
-
- public String root() {
- return root;
- }
-
- public String tenantOverview(TenantName tenantName) {
- return "%s/tenant/%s".formatted(root, tenantName.value());
- }
-
- /** Returns URL to notification settings view for the given tenant */
- public String tenantNotifications(TenantName tenantName) {
- return "%s/tenant/%s/account/notifications".formatted(root, tenantName.value());
- }
-
- public String tenantBilling(TenantName t) { return "%s/tenant/%s/account/billing".formatted(root, t.value()); }
-
- public String tenantBilling(TenantName t, Bill.Id id) { return "%s/bill/%s".formatted(tenantBilling(t), id.value()); }
-
- public String prodApplicationOverview(TenantName tenantName, ApplicationName applicationName) {
- return "%s/tenant/%s/application/%s/prod/instance".formatted(root, tenantName.value(), applicationName.value());
- }
-
- public String instanceOverview(ApplicationId application, Environment environment) {
- return "%s/tenant/%s/application/%s/%s/instance/%s".formatted(root,
- application.tenant().value(),
- application.application().value(),
- environment.isManuallyDeployed() ? environment.value() : "prod",
- application.instance().value());
- }
-
- public String clusterOverview(ApplicationId application, ZoneId zone, ClusterSpec.Id clusterId) {
- return cluster(application, zone, clusterId, null);
- }
-
- public String clusterReindexing(ApplicationId application, ZoneId zone, ClusterSpec.Id clusterId) {
- return cluster(application, zone, clusterId, "reindexing");
- }
-
- public String deploymentRun(RunId id) {
- return "%s/job/%s/run/%s".formatted(
- instanceOverview(id.application(), id.type().environment()), id.type().jobName(), id.number());
- }
-
- /** Returns URL used to request support from the Vespa team. */
- public String support() {
- return root + "/support";
- }
-
- /** Returns URL to verify an email address with the given verification code */
- public String verifyEmail(String verifyCode) {
- return "%s/verify?%s".formatted(root, queryParam("code", verifyCode));
- }
-
- public String termsOfService() { return root + "/terms-of-service-trial.html"; }
-
- private String cluster(ApplicationId application, ZoneId zone, ClusterSpec.Id clusterId, String viewOrNull) {
- return instanceOverview(application, zone.environment()) + '?' +
- queryParam("%s.%s.%s".formatted(application.instance().value(), zone.environment().value(), zone.region().value()),
- "clusters," + clusterId.value() + (viewOrNull == null ? "" : '=' + viewOrNull));
- }
-
- private static String queryParam(String key, String value) {
- return URLEncoder.encode(key, StandardCharsets.UTF_8) + '=' + URLEncoder.encode(value, StandardCharsets.UTF_8);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ControllerIdentityProvider.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ControllerIdentityProvider.java
deleted file mode 100644
index 78e39ad66c6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ControllerIdentityProvider.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
-
-import javax.net.ssl.SSLSocketFactory;
-
-/**
- * @author freva
- */
-public interface ControllerIdentityProvider extends ServiceIdentityProvider {
-
- /** Returns SSLSocketFactory that creates appropriate sockets to talk to the different config servers */
- SSLSocketFactory getConfigServerSslSocketFactory();
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/LogEntry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/LogEntry.java
deleted file mode 100644
index 15132db8157..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/LogEntry.java
+++ /dev/null
@@ -1,122 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import com.yahoo.log.LogLevel;
-
-import java.util.logging.Level;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.UncheckedIOException;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNull;
-
-/**
- * Immutable, simple log entries.
- *
- * @author jonmv
- */
-public class LogEntry {
-
- private final long id;
- private final Instant at;
- private final Type type;
- private final String message;
-
- public LogEntry(long id, Instant at, Type type, String message) {
- if (id < 0)
- throw new IllegalArgumentException("Id must be non-negative, but was " + id + ".");
-
- this.id = id;
- this.at = at;
- this.type = requireNonNull(type);
- this.message = requireNonNull(message);
- }
-
- public long id() {
- return id;
- }
-
- public Instant at() {
- return at;
- }
-
- public Type type() {
- return type;
- }
-
- public String message() {
- return message;
- }
-
- @SuppressWarnings("deprecation")
- public static List<LogEntry> parseVespaLog(InputStream log, Instant from) {
- try (BufferedReader reader = new BufferedReader(new InputStreamReader(log, UTF_8))) {
- return reader.lines()
- .map(line -> line.split("\t"))
- .filter(parts -> parts.length == 7)
- .map(parts -> new LogEntry(0,
- Instant.EPOCH.plus((long) (Double.parseDouble(parts[0]) * 1_000_000), ChronoUnit.MICROS),
- typeOf(LogLevel.parse(parts[5])),
- parts[1] + '\t' + parts[3] + '\t' + parts[4] + '\n' +
- parts[6].replaceAll("\\\\n", "\n")
- .replaceAll("\\\\t", "\t")))
- .filter(entry -> entry.at().isAfter(from))
- .limit(100_000)
- .toList();
- }
- catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
-
- @Override
- public String toString() {
- return "LogEntry{" +
- "id=" + id +
- ", at=" + at.toEpochMilli() +
- ", type=" + type +
- ", message='" + message + '\'' +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof LogEntry)) return false;
- LogEntry entry = (LogEntry) o;
- return id == entry.id &&
- at.toEpochMilli() == entry.at.toEpochMilli() &&
- type == entry.type &&
- Objects.equals(message, entry.message);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id, at, type, message);
- }
-
- @SuppressWarnings("deprecation")
- public static Type typeOf(Level level) {
- return level.intValue() < Level.INFO.intValue() || level.intValue() == LogLevel.IntValEVENT ? Type.debug
- : level.intValue() < Level.WARNING.intValue() ? Type.info
- : level.intValue() < Level.SEVERE.intValue() ? Type.warning
- : Type.error;
- }
-
-
- /** The type of entry, used for rendering. */
- public enum Type {
- debug, info, warning, error, html;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/RunDataStore.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/RunDataStore.java
deleted file mode 100644
index 36f94dc80f1..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/RunDataStore.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-
-import java.io.InputStream;
-import java.util.Optional;
-
-/**
- * @author jonmv
- */
-public interface RunDataStore {
-
- /** Returns the run logs of the given deployment job, if existent. */
- Optional<byte[]> get(RunId id);
-
- /** Stores the given log for the given deployment job. */
- void put(RunId id, byte[] log);
-
- /** Returns the test report og the given deployment job, if present */
- Optional<byte[]> getTestReport(RunId id);
-
- /** Stores the test report for the given deployment job */
- void putTestReport(RunId id, byte[] report);
-
- /** Deletes the run logs and test report for the given deployment job. */
- void delete(RunId id);
-
- /** Deletes all data associated with the given application. */
- void delete(ApplicationId id);
-
- /** Stores Vespa logs for the run. */
- void putLogs(RunId id, boolean tester, InputStream logs);
-
- /** Fetches Vespa logs for the run. */
- InputStream getLogs(RunId id, boolean tester);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java
deleted file mode 100644
index e39a8cf38b7..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java
+++ /dev/null
@@ -1,131 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService;
-import com.yahoo.vespa.hosted.controller.api.integration.artifact.ArtifactRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AccessControlService;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.EnclaveAccessService;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.RoleService;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClient;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingReporter;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationStore;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService;
-import com.yahoo.vespa.hosted.controller.api.integration.entity.EntityService;
-import com.yahoo.vespa.hosted.controller.api.integration.horizon.HorizonClient;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.ContactRetriever;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentIssues;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueHandler;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumer;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.EndpointSecretManager;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.GcpSecretStore;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretService;
-import com.yahoo.vespa.hosted.controller.api.integration.user.RoleMaintainer;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-
-import java.time.Clock;
-import java.util.Optional;
-
-/**
- * This provides access to all service dependencies of the controller. Implementations of this are responsible for
- * constructing and configuring service implementations suitable for use by the controller.
- *
- * @author mpolden
- */
-public interface ServiceRegistry {
-
- ConfigServer configServer();
-
- default Clock clock() { return Clock.systemUTC(); }
-
- default ControllerVersion controllerVersion() { return ControllerVersion.CURRENT; }
-
- default HostName getHostname() { return HostName.of(com.yahoo.net.HostName.getLocalhost()); }
-
- NameService nameService();
-
- VpcEndpointService vpcEndpointService();
-
- Mailer mailer();
-
- EndpointCertificateProvider endpointCertificateProvider();
-
- EndpointCertificateValidator endpointCertificateValidator();
-
- ContactRetriever contactRetriever();
-
- IssueHandler issueHandler();
-
- OwnershipIssues ownershipIssues();
-
- DeploymentIssues deploymentIssues();
-
- EntityService entityService();
-
- CostReportConsumer costReportConsumer();
-
- ArtifactRepository artifactRepository();
-
- TesterCloud testerCloud();
-
- ApplicationStore applicationStore();
-
- RunDataStore runDataStore();
-
- ZoneRegistry zoneRegistry();
-
- ConsoleUrls consoleUrls();
-
- ResourceTagger resourceTagger();
-
- EnclaveAccessService enclaveAccessService();
-
- RoleService roleService();
-
- SystemMonitor systemMonitor();
-
- BillingController billingController();
-
- ResourceDatabaseClient resourceDatabase();
-
- BillingDatabaseClient billingDatabase();
-
- Optional<? extends ArtifactRegistry> artifactRegistry(CloudName cloudName);
-
- TenantSecretService tenantSecretService();
-
- EndpointSecretManager secretManager();
-
- ArchiveService archiveService();
-
- ChangeRequestClient changeRequestClient();
-
- AccessControlService accessControlService();
-
- HorizonClient horizonClient();
-
- PlanRegistry planRegistry();
-
- RoleMaintainer roleMaintainer();
-
- GcpSecretStore gcpSecretStore();
-
- BillingReporter billingReporter();
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBuckets.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBuckets.java
deleted file mode 100644
index cbd4508cdde..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveBuckets.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.archive;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * @author freva
- */
-public record ArchiveBuckets(Set<VespaManagedArchiveBucket> vespaManaged,
- Set<TenantManagedArchiveBucket> tenantManaged) {
- public static final ArchiveBuckets EMPTY = new ArchiveBuckets(Set.of(), Set.of());
-
- public ArchiveBuckets(Set<VespaManagedArchiveBucket> vespaManaged, Set<TenantManagedArchiveBucket> tenantManaged) {
- this.vespaManaged = Set.copyOf(vespaManaged);
- this.tenantManaged = Set.copyOf(tenantManaged);
- }
-
- /** Adds or replaces a VespaManagedArchive bucket with the given archive bucket */
- public ArchiveBuckets with(VespaManagedArchiveBucket vespaManagedArchiveBucket) {
- Set<VespaManagedArchiveBucket> updated = new HashSet<>(vespaManaged);
- updated.removeIf(bucket -> bucket.bucketName().equals(vespaManagedArchiveBucket.bucketName()));
- updated.add(vespaManagedArchiveBucket);
- return new ArchiveBuckets(updated, tenantManaged);
- }
-
- /** Adds or replaces a TenantManagedArchive bucket with the given archive bucket */
- public ArchiveBuckets with(TenantManagedArchiveBucket tenantManagedArchiveBucket) {
- Set<TenantManagedArchiveBucket> updated = new HashSet<>(tenantManaged);
- updated.removeIf(bucket -> bucket.cloudAccount().equals(tenantManagedArchiveBucket.cloudAccount()));
- updated.add(tenantManagedArchiveBucket);
- return new ArchiveBuckets(vespaManaged, updated);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java
deleted file mode 100644
index a4c5c7a0037..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveService.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.archive;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-
-import java.net.URI;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Stream;
-
-/**
- * Service that manages archive storage URIs for tenant nodes.
- *
- * @author freva
- * @author andreer
- */
-public interface ArchiveService {
-
- VespaManagedArchiveBucket createArchiveBucketFor(ZoneId zoneId);
-
- void updatePolicies(ZoneId zoneId, Set<VespaManagedArchiveBucket> buckets, Map<TenantName,ArchiveAccess> authorizeAccessByTenantName);
-
- boolean canAddTenantToBucket(ZoneId zoneId, VespaManagedArchiveBucket bucket);
-
- Optional<String> findEnclaveArchiveBucket(ZoneId zoneId, CloudAccount cloudAccount);
-
- URI bucketURI(ZoneId zoneId, String bucketName);
-
- /**
- * @return the version of the template that was used during the last apply for the given cloud account,
- * or {@link Version#emptyVersion} if the version tag was not present or invalid,
- * or {@link Optional#empty()} if the we have no access to the cloud account (template probably not applied yet)
- */
- Optional<Version> getEnclaveTemplateVersion(CloudAccount cloudAccount);
-
- static Stream<Version> parseVersion(String versionString) {
- try {
- return Stream.of(Version.fromString(versionString));
- } catch (IllegalArgumentException e) {
- return Stream.empty();
- }
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveUriUpdate.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveUriUpdate.java
deleted file mode 100644
index 833c2acb79a..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/ArchiveUriUpdate.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.archive;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.TenantName;
-
-import java.net.URI;
-import java.util.Optional;
-
-/**
- * Represents an operation to update or unset the archive URI value for a given tenant or cloud account.
- *
- * @author freva
- */
-public class ArchiveUriUpdate {
- private final Optional<TenantName> tenantName;
- private final Optional<CloudAccount> cloudAccount;
- private final Optional<URI> archiveUri;
-
- private ArchiveUriUpdate(Optional<TenantName> tenantName, Optional<CloudAccount> cloudAccount, Optional<URI> archiveUri) {
- this.tenantName = tenantName;
- this.cloudAccount = cloudAccount;
- this.archiveUri = archiveUri;
- }
-
- public Optional<TenantName> tenantName() { return tenantName; }
- public Optional<CloudAccount> cloudAccount() { return cloudAccount; }
- public Optional<URI> archiveUri() { return archiveUri; }
-
- public static ArchiveUriUpdate setArchiveUriFor(TenantName tenantName, URI archiveUri) {
- return new ArchiveUriUpdate(Optional.of(tenantName), Optional.empty(), Optional.of(archiveUri));
- }
- public static ArchiveUriUpdate deleteArchiveUriFor(TenantName tenantName) {
- return new ArchiveUriUpdate(Optional.of(tenantName), Optional.empty(), Optional.empty());
- }
-
- public static ArchiveUriUpdate setArchiveUriFor(CloudAccount cloudAccount, URI archiveUri) {
- return new ArchiveUriUpdate(Optional.empty(), Optional.of(cloudAccount), Optional.of(archiveUri));
- }
- public static ArchiveUriUpdate deleteArchiveUriFor(CloudAccount cloudAccount) {
- return new ArchiveUriUpdate(Optional.empty(), Optional.of(cloudAccount), Optional.empty());
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java
deleted file mode 100644
index c4cb909d724..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/MockArchiveService.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.archive;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-
-import java.net.URI;
-import java.time.Clock;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * @author freva
- * @author andreer
- */
-public class MockArchiveService implements ArchiveService {
-
- private final Map<ZoneId, Set<TenantManagedArchiveBucket>> tenantArchiveBucketsByZone = new HashMap<>();
- public Set<VespaManagedArchiveBucket> archiveBuckets = new HashSet<>();
- public Map<TenantName, ArchiveAccess> authorizeAccessByTenantName = new HashMap<>();
-
- private final Clock clock;
-
- public MockArchiveService(Clock clock) {
- this.clock = clock;
- }
-
- @Override
- public VespaManagedArchiveBucket createArchiveBucketFor(ZoneId zoneId) {
- return new VespaManagedArchiveBucket("bucketName", "keyArn");
- }
-
- @Override
- public void updatePolicies(ZoneId zoneId, Set<VespaManagedArchiveBucket> buckets, Map<TenantName, ArchiveAccess> authorizeAccessByTenantName) {
- this.archiveBuckets = new HashSet<>(buckets);
- this.authorizeAccessByTenantName = new HashMap<>(authorizeAccessByTenantName);
- }
-
- @Override
- public boolean canAddTenantToBucket(ZoneId zoneId, VespaManagedArchiveBucket bucket) {
- return bucket.tenants().size() < 5;
- }
-
- @Override
- public Optional<String> findEnclaveArchiveBucket(ZoneId zoneId, CloudAccount cloudAccount) {
- return tenantArchiveBucketsByZone.getOrDefault(zoneId, Set.of()).stream()
- .filter(bucket -> bucket.cloudAccount().equals(cloudAccount))
- .findFirst()
- .map(TenantManagedArchiveBucket::bucketName);
- }
-
- @Override
- public URI bucketURI(ZoneId zoneId, String bucketName) {
- return URI.create(String.format("s3://%s/", bucketName));
- }
-
- @Override
- public Optional<Version> getEnclaveTemplateVersion(CloudAccount cloudAccount) {
- return Optional.of(new Version(1, 2, 3));
- }
-
-
- public void setEnclaveArchiveBucket(ZoneId zoneId, CloudAccount cloudAccount, String bucketName) {
- removeEnclaveArchiveBucket(zoneId, cloudAccount);
- tenantArchiveBucketsByZone.computeIfAbsent(zoneId, z -> new HashSet<>())
- .add(new TenantManagedArchiveBucket(bucketName, cloudAccount, clock.instant()));
- }
-
- public void removeEnclaveArchiveBucket(ZoneId zoneId, CloudAccount cloudAccount) {
- Optional.ofNullable(tenantArchiveBucketsByZone.get(zoneId))
- .ifPresent(set -> set.removeIf(bucket -> bucket.cloudAccount().equals(cloudAccount)));
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/TenantManagedArchiveBucket.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/TenantManagedArchiveBucket.java
deleted file mode 100644
index a71178ed28f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/TenantManagedArchiveBucket.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.archive;
-
-import com.yahoo.config.provision.CloudAccount;
-
-import java.time.Instant;
-
-/**
- * Represents a cloud storage bucket (e.g. AWS S3 or Google Storage) used to store archive data - logs, heap/core dumps, etc.
- * that is managed by the tenant directly.
- *
- * @author freva
- */
-public record TenantManagedArchiveBucket(String bucketName, CloudAccount cloudAccount, Instant updatedAt) {
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/VespaManagedArchiveBucket.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/VespaManagedArchiveBucket.java
deleted file mode 100644
index 3bcb41cbb68..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/VespaManagedArchiveBucket.java
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.archive;
-
-import com.google.common.collect.Sets;
-import com.yahoo.config.provision.TenantName;
-
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * Represents a cloud storage bucket (e.g. AWS S3 or Google Storage) used to store archive data - logs, heap/core dumps, etc.
- * that is managed by the Vespa controller.
- *
- * @author andreer
- */
-public class VespaManagedArchiveBucket {
- private final String bucketName;
- private final String keyArn;
- private final Set<TenantName> tenants;
-
- public VespaManagedArchiveBucket(String bucketName, String keyArn) {
- this(bucketName, keyArn, Set.of());
- }
-
- private VespaManagedArchiveBucket(String bucketName, String keyArn, Set<TenantName> tenants) {
- this.bucketName = bucketName;
- this.keyArn = keyArn;
- this.tenants = Set.copyOf(tenants);
- }
-
- public String bucketName() {
- return bucketName;
- }
-
- public String keyArn() {
- return keyArn;
- }
-
- public Set<TenantName> tenants() {
- return tenants;
- }
-
- public VespaManagedArchiveBucket withTenant(TenantName tenant) {
- return withTenants(Set.of(tenant));
- }
-
- public VespaManagedArchiveBucket withTenants(Set<TenantName> tenants) {
- return new VespaManagedArchiveBucket(bucketName, keyArn, Sets.union(this.tenants, tenants));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- VespaManagedArchiveBucket that = (VespaManagedArchiveBucket) o;
- return bucketName.equals(that.bucketName) && keyArn.equals(that.keyArn) && tenants.equals(that.tenants);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(bucketName, keyArn, tenants);
- }
-
- @Override
- public String toString() {
- return "ArchiveBucket{" +
- "bucketName='" + bucketName + '\'' +
- ", keyArn='" + keyArn + '\'' +
- ", tenants=" + tenants +
- '}';
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/package-info.java
deleted file mode 100644
index 7fb12f87b4a..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/archive/package-info.java
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author freva
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.archive;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/Artifact.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/Artifact.java
deleted file mode 100644
index 2c1ea1cb3a7..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/Artifact.java
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.artifact;
-
-import com.yahoo.component.Version;
-
-import java.time.Instant;
-import java.util.Objects;
-
-/**
- * A registry artifact (e.g. container image or RPM)
- *
- * @author mpolden
- */
-public class Artifact {
-
- private final String id;
- private final String registry;
- private final String repository;
- private final String tag;
- private final Instant createdAt;
- private final Version version;
-
- public Artifact(String id, String registry, String repository, String tag, Instant createdAt, Version version) {
- this.id = Objects.requireNonNull(id);
- this.registry = Objects.requireNonNull(registry);
- this.repository = Objects.requireNonNull(repository);
- this.tag = Objects.requireNonNull(tag);
- this.createdAt = Objects.requireNonNull(createdAt);
- this.version = Objects.requireNonNull(version);
- }
-
- /** Unique identifier of this */
- public String id() {
- return id;
- }
-
- /** The registry holding this artifact */
- public String registry() {
- return registry;
- }
-
- /** Repository of this artifact */
- public String repository() {
- return repository;
- }
-
- /** Tag of this artifact */
- public String tag() {
- return tag;
- }
-
- /** The time this was created */
- public Instant createdAt() {
- return createdAt;
- }
-
- /** The version of this */
- public Version version() {
- return version;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Artifact that = (Artifact) o;
- return id.equals(that.id) &&
- registry.equals(that.registry) &&
- repository.equals(that.repository) &&
- tag.equals(that.tag) &&
- createdAt.equals(that.createdAt) &&
- version.equals(that.version);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id, registry, repository, tag, createdAt, version);
- }
-
- @Override
- public String toString() {
- return "artifact " + registry + "/" + repository + " [version=" + version.toFullString() + ",createdAt=" + createdAt + ",tag=" + tag + "]";
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/ArtifactRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/ArtifactRegistry.java
deleted file mode 100644
index 48391e1670e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/ArtifactRegistry.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.artifact;
-
-
-import java.util.List;
-
-/**
- * A registry of artifacts (e.g. container image or RPM).
- *
- * @author mpolden
- */
-public interface ArtifactRegistry {
-
- /** Delete all given artifacts */
- void deleteAll(List<Artifact> artifacts);
-
- /** Returns a list of all artifacts in this system */
- List<Artifact> list();
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/package-info.java
deleted file mode 100644
index a7b3d744664..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/artifact/package-info.java
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author mpolden
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.artifact;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AccessControlService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AccessControlService.java
deleted file mode 100644
index 1335f50044e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AccessControlService.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.athenz.api.AthenzRoleInformation;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-
-import java.time.Instant;
-import java.util.Collection;
-
-/**
- * Manage operator data plane access control
- *
- * @author mortent
- */
-public interface AccessControlService {
- boolean approveDataPlaneAccess(AthenzUser user, Instant expiry);
- boolean decideSshAccess(TenantName tenantName, Instant expiry, OAuthCredentials oAuthCredentials, boolean approve);
- boolean requestSshAccess(TenantName tenantName);
- AthenzRoleInformation getAccessRoleInformation(TenantName tenantName);
- void setManagedAccess(TenantName tenantName, boolean managedAccess);
- boolean getManagedAccess(TenantName tenantName);
- Collection<AthenzUser> listMembers();
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ApplicationAction.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ApplicationAction.java
deleted file mode 100644
index 42dd0aea65b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ApplicationAction.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-/**
- * @author bjorncs
- */
-public enum ApplicationAction {
- deploy("deployer"),
- read("reader"),
- write("writer");
-
- public final String roleName;
-
- ApplicationAction(String roleName) {
- this.roleName = roleName;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzAccessControlService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzAccessControlService.java
deleted file mode 100644
index c639928e3cc..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzAccessControlService.java
+++ /dev/null
@@ -1,161 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.athenz.api.AthenzAssertion;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzGroup;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzRole;
-import com.yahoo.vespa.athenz.api.AthenzRoleInformation;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-import com.yahoo.vespa.athenz.client.zms.ZmsClient;
-
-import java.time.Instant;
-import java.util.Collection;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-public class AthenzAccessControlService implements AccessControlService {
-
- private static final String ALLOWED_OPERATOR_GROUPNAME = "vespa-team";
- private static final String DATAPLANE_ACCESS_ROLENAME = "operator-data-plane";
- private final String TENANT_DOMAIN_PREFIX = "vespa.tenant";
- private final String ACCESS_APPROVAL_POLICY = "vespa-access-requester";
- private final ZmsClient zmsClient;
- private final AthenzRole dataPlaneAccessRole;
- private final AthenzGroup vespaTeam;
- private final Optional<ZmsClient> vespaZmsClient;
- private final AthenzInstanceSynchronizer athenzInstanceSynchronizer;
-
-
- public AthenzAccessControlService(ZmsClient zmsClient, AthenzDomain domain, Optional<ZmsClient> vespaZmsClient, AthenzInstanceSynchronizer athenzInstanceSynchronizer) {
- this.zmsClient = zmsClient;
- this.vespaZmsClient = vespaZmsClient;
- this.athenzInstanceSynchronizer = athenzInstanceSynchronizer;
- this.dataPlaneAccessRole = new AthenzRole(domain, DATAPLANE_ACCESS_ROLENAME);
- this.vespaTeam = new AthenzGroup(domain, ALLOWED_OPERATOR_GROUPNAME);
- }
-
- @Override
- public boolean approveDataPlaneAccess(AthenzUser user, Instant expiry) {
- // Can only approve team members, other members must be manually approved
- if(!isVespaTeamMember(user)) {
- throw new IllegalArgumentException(String.format("User %s requires manual approval, please contact Vespa team", user.getName()));
- }
- Map<AthenzIdentity, String> users = zmsClient.listPendingRoleApprovals(dataPlaneAccessRole);
- if (users.containsKey(user)) {
- zmsClient.decidePendingRoleMembership(dataPlaneAccessRole, user, expiry, Optional.empty(), Optional.empty(), true);
- return true;
- }
- return false;
- }
-
- @Override
- // Return list of approved members (users, excluding services) of data plane role
- public Collection<AthenzUser> listMembers() {
- return zmsClient.listMembers(dataPlaneAccessRole)
- .stream().filter(AthenzUser.class::isInstance)
- .map(AthenzUser.class::cast)
- .toList();
- }
-
- /**
- * @return Whether the ssh access role has any pending role membership requests
- */
- @Override
- public AthenzRoleInformation getAccessRoleInformation(TenantName tenantName) {
- return vespaZmsClient.map(
- zms -> {
- var role = sshRole(tenantName);
- return zms.getFullRoleInformation(role);
- }
- ).orElseThrow(() -> new UnsupportedOperationException("Only allowed in systems running Vespa Athenz instance"));
-
- }
-
- /**
- * @return true if access has been granted - false if already member
- */
- @Override
- public boolean decideSshAccess(TenantName tenantName, Instant expiry, OAuthCredentials oAuthCredentials, boolean approve) {
- return vespaZmsClient.map(
- zms -> {
- var role = sshRole(tenantName);
-
- var roleInformation = zms.getFullRoleInformation(role);
- if (roleInformation.getPendingRequest().isEmpty())
- return false;
- var reason = roleInformation.getPendingRequest().get().getReason();
-
- zms.decidePendingRoleMembership(role, vespaTeam, expiry, Optional.of(reason), Optional.of(oAuthCredentials), approve);
- if (approve) athenzInstanceSynchronizer.synchronizeInstances(tenantName);
- return true;
- }
- ).orElseThrow(() -> new UnsupportedOperationException("Only allowed in systems running Vespa Athenz instance"));
- }
-
- /**
- * @return true if access has been requested - false if already member
- */
- @Override
- public boolean requestSshAccess(TenantName tenantName) {
- return vespaZmsClient.map(
- zms -> {
- var role = sshRole(tenantName);
-
- if (zms.getMembership(role, vespaTeam))
- return false;
-
- zms.addRoleMember(role, vespaTeam, Optional.empty());
- return true;
- }
- ).orElseThrow(() -> new UnsupportedOperationException("Only allowed in systems running Vespa Athenz instance"));
- }
-
- public void setManagedAccess(TenantName tenantName, boolean managedAccess) {
- vespaZmsClient.ifPresentOrElse(
- zms -> {
- var role = sshRole(tenantName);
- var assertion = getApprovalAssertion(role);
- if (managedAccess) {
- zms.deletePolicyRule(role.domain(), ACCESS_APPROVAL_POLICY, assertion.action(), assertion.resource(), assertion.role());
- } else {
- zms.addPolicyRule(role.domain(), ACCESS_APPROVAL_POLICY, assertion.action(), assertion.resource(), assertion.role());
- }
- },() -> { throw new UnsupportedOperationException("Only allowed in systems running Vespa Athenz instance"); });
- }
-
- public boolean getManagedAccess(TenantName tenantName) {
- return vespaZmsClient.map(
- zms -> {
- var role = sshRole(tenantName);
- var approvalAssertion = getApprovalAssertion(role);
- return zms.getPolicy(role.domain(), ACCESS_APPROVAL_POLICY)
- .map(policy -> policy.assertions().stream().noneMatch(assertion -> assertion.satisfies(approvalAssertion)))
- .orElse(true);
- }).orElseThrow(() -> new UnsupportedOperationException("Only allowed in systems running Vespa Athenz instance") );
- }
-
- private AthenzRole sshRole(TenantName tenantName) {
- return new AthenzRole(getTenantDomain(tenantName), "ssh_access");
- }
-
- private AthenzDomain getTenantDomain(TenantName tenantName) {
- return new AthenzDomain(TENANT_DOMAIN_PREFIX + "." + tenantName.value());
- }
-
- public boolean isVespaTeamMember(AthenzUser user) {
- return zmsClient.getGroupMembership(vespaTeam, user);
- }
-
- private AthenzAssertion getApprovalAssertion(AthenzRole accessRole) {
- var approverRole = new AthenzRole(accessRole.domain(), "vespa-access-approver");
- return AthenzAssertion.newBuilder(approverRole, accessRole.toResourceName(), "update_members")
- .effect(AthenzAssertion.Effect.ALLOW)
- .build();
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzClientFactory.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzClientFactory.java
deleted file mode 100644
index 448598fd590..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzClientFactory.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.client.zms.ZmsClient;
-import com.yahoo.vespa.athenz.client.zts.ZtsClient;
-
-/**
- * @author bjorncs
- */
-public interface AthenzClientFactory {
-
- AthenzIdentity getControllerIdentity();
-
- ZmsClient createZmsClient();
-
- ZtsClient createZtsClient();
-
- default boolean cacheLookups() { return false; }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzClientFactoryMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzClientFactoryMock.java
deleted file mode 100644
index 2c53598a198..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzClientFactoryMock.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.vespa.athenz.api.AthenzService;
-import com.yahoo.vespa.athenz.client.zms.ZmsClient;
-import com.yahoo.vespa.athenz.client.zts.ZtsClient;
-
-/**
- * @author bjorncs
- */
-public class AthenzClientFactoryMock extends AbstractComponent implements AthenzClientFactory {
-
- private final AthenzDbMock athenz;
-
- @Inject
- public AthenzClientFactoryMock() {
- this(new AthenzDbMock());
- }
-
- public AthenzClientFactoryMock(AthenzDbMock athenz) {
- this.athenz = athenz;
- }
-
- public AthenzDbMock getSetup() {
- return athenz;
- }
-
- @Override
- public AthenzService getControllerIdentity() {
- return new AthenzService("vespa.hosting");
- }
-
- @Override
- public ZmsClient createZmsClient() {
- return new ZmsClientMock(athenz, getControllerIdentity());
- }
-
- @Override
- public ZtsClient createZtsClient() {
- return new ZtsClientMock(athenz, createZmsClient());
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java
deleted file mode 100644
index 1bbb1886871..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzDbMock.java
+++ /dev/null
@@ -1,298 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.regex.Pattern;
-
-/**
- * @author bjorncs
- */
-public class AthenzDbMock {
-
- public final Map<AthenzDomain, Domain> domains = new HashMap<>();
- public final List<AthenzIdentity> hostedOperators = new ArrayList<>();
-
- public AthenzDbMock addDomain(Domain domain) {
- domains.put(domain.name, domain);
- return this;
- }
-
- public Domain getOrCreateDomain(AthenzDomain domain) {
- return this.getOrCreateDomain(domain, Map.of());
- }
-
- public Domain getOrCreateDomain(AthenzDomain domain, Map<String, Object> attributes) {
- return domains.computeIfAbsent(domain, Domain::new).withAttributes(attributes);
- }
-
- public AthenzDbMock addHostedOperator(AthenzIdentity athenzIdentity) {
- hostedOperators.add(athenzIdentity);
- return this;
- }
-
- public static class Domain {
-
- public final AthenzDomain name;
- public final Set<AthenzIdentity> admins = new HashSet<>();
- public final Set<AthenzIdentity> tenantAdmins = new HashSet<>();
- public final Map<ApplicationId, Application> applications = new HashMap<>();
- public final Map<String, Service> services = new HashMap<>();
- public final List<Role> roles = new ArrayList<>();
- public final Map<String, Policy> policies = new HashMap<>();
- public boolean isVespaTenant = false;
- public final Map<String, Object> attributes = new HashMap<>();
-
- public Domain(AthenzDomain name) {
- this.name = name;
- }
-
- public Domain admin(AthenzIdentity identity) {
- admins.add(identity);
- policies.put("admin", new Policy("admin", identity.getFullName(), ".*", ".*"));
- return this;
- }
-
- public Domain tenantAdmin(AthenzIdentity identity) {
- tenantAdmins.add(identity);
- return this;
- }
-
- public Domain deleteTenantAdmin(AthenzIdentity identity) {
- tenantAdmins.remove(identity);
- return this;
- }
-
- public Domain withPolicy(String name, String principalRegex, String operation, String resource) {
- policies.put(name, new Policy(name, principalRegex, operation, resource));
- return this;
- }
-
- public Domain withAttributes(Map<String, Object> attributes) {
- this.attributes.putAll(attributes);
- return this;
- }
-
- /**
- * Simulates establishing Vespa tenancy in Athens.
- */
- public void markAsVespaTenant() {
- isVespaTenant = true;
- }
-
- public boolean hasRole(String name) { return roles.stream().anyMatch(r -> r.name.equals(name)); }
-
- public boolean hasPolicy(String name) { return policies.containsKey(name); }
-
- public boolean checkAccess(AthenzIdentity principal, String action, String resource) {
- return policies.values().stream().anyMatch(a -> a.matches(principal, action, resource));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Domain domain = (Domain) o;
- return isVespaTenant == domain.isVespaTenant && Objects.equals(name, domain.name) && Objects.equals(admins, domain.admins) && Objects.equals(tenantAdmins, domain.tenantAdmins) && Objects.equals(applications, domain.applications) && Objects.equals(services, domain.services) && Objects.equals(roles, domain.roles) && Objects.equals(policies, domain.policies) && Objects.equals(attributes, domain.attributes);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name, admins, tenantAdmins, applications, services, roles, policies, isVespaTenant, attributes);
- }
-
- @Override
- public String toString() {
- return "Domain{" +
- "name=" + name +
- ", admins=" + admins +
- ", tenantAdmins=" + tenantAdmins +
- ", applications=" + applications +
- ", services=" + services +
- ", roles=" + roles +
- ", policies=" + policies +
- ", isVespaTenant=" + isVespaTenant +
- ", attributes=" + attributes +
- '}';
- }
-
- }
-
- public static class Application {
-
- public final Map<ApplicationAction, Set<AthenzIdentity>> acl = new HashMap<>();
-
- public Application() {
- acl.put(ApplicationAction.deploy, new HashSet<>());
- acl.put(ApplicationAction.read, new HashSet<>());
- acl.put(ApplicationAction.write, new HashSet<>());
- }
-
- public Application addRoleMember(ApplicationAction action, AthenzIdentity identity) {
- acl.get(action).add(identity);
- return this;
- }
- }
-
- public static class Service {
-
- public final boolean allowLaunch;
-
- public Service(boolean allowLaunch) {
- this.allowLaunch = allowLaunch;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Service service = (Service) o;
- return allowLaunch == service.allowLaunch;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(allowLaunch);
- }
-
- @Override
- public String toString() {
- return allowLaunch ? "allowed" : "denied";
- }
-
- }
-
- public static class Policy {
- private final String name;
- public final List<Assertion> assertions = new ArrayList<>();
-
- public Policy(String name, String principal, String action, String resource) {
- this(name);
- this.assertions.add(new Assertion("grant", principal, action, resource));
- }
-
- public Policy(String name) { this.name = name; }
-
- public String name() {
- return name;
- }
-
- public boolean matches(String assertion) {
- return assertions.stream().anyMatch(a -> a.matches(assertion));
- }
-
- public boolean matches(AthenzIdentity principal, String action, String resource) {
- return assertions.stream().anyMatch(a -> a.matches(principal, action, resource));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Policy policy = (Policy) o;
- return Objects.equals(name, policy.name) && Objects.equals(assertions, policy.assertions);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name, assertions);
- }
-
- @Override
- public String toString() {
- return name + ": " + assertions;
- }
-
- }
-
- public static class Assertion {
- private final String effect;
- private final String role;
- private final String action;
- private final String resource;
-
- public Assertion(String effect, String role, String action, String resource) {
- this.effect = effect;
- this.role = role;
- this.action = action;
- this.resource = resource;
- }
-
- public Assertion(String role, String action, String resource) { this("allow", role, action, resource); }
-
- public String effect() { return effect; }
- public String role() { return role; }
- public String action() { return action; }
- public String resource() { return resource; }
-
- public boolean matches(AthenzIdentity principal, String action, String resource) {
- return Pattern.compile(this.role).matcher(principal.getFullName()).matches()
- && Pattern.compile(this.action).matcher(action).matches()
- && Pattern.compile(this.resource).matcher(resource).matches();
- }
-
- public boolean matches(String assertion) { return asString().equals(assertion); }
-
- public String asString() { return String.format("%s %s to %s on %s", effect, action, role, resource).toLowerCase(); }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Assertion assertion = (Assertion) o;
- return Objects.equals(effect, assertion.effect) && Objects.equals(role, assertion.role)
- && Objects.equals(action, assertion.action) && Objects.equals(resource, assertion.resource);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(effect, role, action, resource);
- }
-
- @Override
- public String toString() {
- return asString();
- }
-
- }
-
- public static class Role {
- private final String name;
-
- public Role(String name) {
- this.name = name;
- }
-
- public String name() {
- return name;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Role role = (Role) o;
- return Objects.equals(name, role.name);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name);
- }
-
- @Override
- public String toString() {
- return name;
- }
-
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizer.java
deleted file mode 100644
index f83fffc2539..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizer.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.config.provision.TenantName;
-
-/**
- * @author olaa
- *
- * Responsible for synchronizing misc roles and their pending memberships between separate Athenz instances
- */
-public interface AthenzInstanceSynchronizer {
-
- void synchronizeInstances(TenantName tenant);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizerMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizerMock.java
deleted file mode 100644
index f6233c5b4c7..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/AthenzInstanceSynchronizerMock.java
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.config.provision.TenantName;
-
-/**
- * @author olaa
- */
-public class AthenzInstanceSynchronizerMock implements AthenzInstanceSynchronizer {
- @Override
- public void synchronizeInstances(TenantName tenant) {}
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/MockAccessControlService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/MockAccessControlService.java
deleted file mode 100644
index a8fb464973e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/MockAccessControlService.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzRoleInformation;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-
-import java.time.Instant;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-public class MockAccessControlService implements AccessControlService {
-
- private final Set<AthenzUser> pendingMembers = new HashSet<>();
- private final Set<AthenzUser> members = new HashSet<>();
-
- @Override
- public boolean approveDataPlaneAccess(AthenzUser user, Instant expiry) {
- if (pendingMembers.remove(user)) {
- return members.add(user);
- } else {
- return false;
- }
- }
-
- @Override
- public Collection<AthenzUser> listMembers() {
- return Set.copyOf(members);
- }
-
- @Override
- public boolean decideSshAccess(TenantName tenantName, Instant expiry, OAuthCredentials oAuthCredentials, boolean approve) {
- return false;
- }
-
- @Override
- public boolean requestSshAccess(TenantName tenantName) {
- return false;
- }
-
- @Override
- public AthenzRoleInformation getAccessRoleInformation(TenantName tenantName) {
- return new AthenzRoleInformation(new AthenzDomain("test-domain"), "tenant-role", false, false, Optional.empty(), List.of());
- }
-
- @Override
- public void setManagedAccess(TenantName tenantName, boolean managedAccess) {
-
- }
-
- @Override
- public boolean getManagedAccess(TenantName tenant) {
- return false;
- }
-
- public void addPendingMember(AthenzUser user) {
- pendingMembers.add(user);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java
deleted file mode 100644
index 6e2486c1c01..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZmsClientMock.java
+++ /dev/null
@@ -1,362 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.vespa.athenz.api.AthenzAssertion;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzDomainMeta;
-import com.yahoo.vespa.athenz.api.AthenzGroup;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzPolicy;
-import com.yahoo.vespa.athenz.api.AthenzResourceName;
-import com.yahoo.vespa.athenz.api.AthenzRole;
-import com.yahoo.vespa.athenz.api.AthenzRoleInformation;
-import com.yahoo.vespa.athenz.api.AthenzService;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-import com.yahoo.vespa.athenz.client.zms.QuotaUsage;
-import com.yahoo.vespa.athenz.client.zms.RoleAction;
-import com.yahoo.vespa.athenz.client.zms.ZmsClient;
-import com.yahoo.vespa.athenz.client.zms.ZmsClientException;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
-
-import java.security.PublicKey;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-/**
- * @author bjorncs
- */
-public class ZmsClientMock implements ZmsClient {
-
- private static final Logger log = Logger.getLogger(ZmsClientMock.class.getName());
-
- private final AthenzDbMock athenz;
- private final AthenzIdentity controllerIdentity;
- private static final Pattern TENANT_RESOURCE_PATTERN = Pattern.compile("service\\.hosting\\.tenant\\.(?<tenantDomain>[\\w\\-_]+)\\..*");
- private static final Pattern APPLICATION_RESOURCE_PATTERN = Pattern.compile("service\\.hosting\\.tenant\\.[\\w\\-_]+\\.res_group\\.(?<resourceGroup>[\\w\\-_]+)\\.(?<environment>[\\w\\-_]+)");
-
- public ZmsClientMock(AthenzDbMock athenz, AthenzIdentity controllerIdentity) {
- this.athenz = athenz;
- this.controllerIdentity = controllerIdentity;
- }
-
- @Override
- public void createTenancy(AthenzDomain tenantDomain, AthenzIdentity providerService, OAuthCredentials oAuthCredentials) {
- log("createTenancy(tenantDomain='%s')", tenantDomain);
- getDomainOrThrow(tenantDomain, false).isVespaTenant = true;
- }
-
- @Override
- public void deleteTenancy(AthenzDomain tenantDomain, AthenzIdentity providerService, OAuthCredentials oAuthCredentials) {
- log("deleteTenancy(tenantDomain='%s')", tenantDomain);
- AthenzDbMock.Domain domain = getDomainOrThrow(tenantDomain, false);
- domain.isVespaTenant = false;
- domain.applications.clear();
- domain.tenantAdmins.clear();
- }
-
- @Override
- public void createProviderResourceGroup(AthenzDomain tenantDomain, AthenzIdentity providerService, String resourceGroup,
- Set<RoleAction> roleActions, OAuthCredentials oAuthCredentials) {
- log("createProviderResourceGroup(tenantDomain='%s', resourceGroup='%s')", tenantDomain, resourceGroup);
- AthenzDbMock.Domain domain = getDomainOrThrow(tenantDomain, true);
- ApplicationId applicationId = new ApplicationId(resourceGroup);
- if (!domain.applications.containsKey(applicationId)) {
- domain.applications.put(applicationId, new AthenzDbMock.Application());
- }
- }
-
- @Override
- public void deleteProviderResourceGroup(AthenzDomain tenantDomain, AthenzIdentity providerService, String resourceGroup,
- OAuthCredentials oAuthCredentials) {
- log("deleteProviderResourceGroup(tenantDomain='%s', resourceGroup='%s')", tenantDomain, resourceGroup);
- getDomainOrThrow(tenantDomain, true).applications.remove(new ApplicationId(resourceGroup));
- }
-
- @Override
- public void createTenantResourceGroup(AthenzDomain tenantDomain, AthenzIdentity provider, String resourceGroup,
- Set<RoleAction> roleActions) {
- log("createTenantResourceGroup(tenantDomain='%s', resourceGroup='%s')", tenantDomain, resourceGroup);
- AthenzDbMock.Domain domain = getDomainOrThrow(tenantDomain, true);
- ApplicationId applicationId = new ApplicationId(resourceGroup);
- if (!domain.applications.containsKey(applicationId)) {
- domain.applications.put(applicationId, new AthenzDbMock.Application());
- }
- }
-
- @Override
- public Set<RoleAction> getTenantResourceGroups(AthenzDomain tenantDomain, AthenzIdentity provider, String resourceGroup) {
- Set<RoleAction> result = new HashSet<>();
- getDomainOrThrow(tenantDomain, true).applications.get(resourceGroup).acl
- .forEach((role, roleMembers) -> result.add(new RoleAction(role.roleName, role.roleName)));
- return result;
- }
-
- @Override
- public void addRoleMember(AthenzRole role, AthenzIdentity member, Optional<String> reason) {
- if ( ! role.roleName().equals("tenancy.vespa.hosting.admin"))
- throw new IllegalArgumentException("Mock only supports adding tenant admins, not " + role.roleName());
- getDomainOrThrow(role.domain(), true).tenantAdmin(member);
- }
-
- @Override
- public void deleteRoleMember(AthenzRole role, AthenzIdentity member) {
- if ( ! role.roleName().equals("tenancy.vespa.hosting.admin"))
- throw new IllegalArgumentException("Mock only supports deleting tenant admins, not " + role.roleName());
- getDomainOrThrow(role.domain(), true).deleteTenantAdmin(member);
- }
-
- @Override
- public boolean getMembership(AthenzRole role, AthenzIdentity identity) {
- if (role.roleName().equals("admin")) {
- return getDomainOrThrow(role.domain(), false).admins.contains(identity);
- }
- return false;
- }
-
- @Override
- public boolean getGroupMembership(AthenzGroup group, AthenzIdentity identity) {
- return false;
- }
-
- @Override
- public List<AthenzDomain> getDomainList(String prefix) {
- log("getDomainList()");
- return new ArrayList<>(athenz.domains.keySet());
- }
-
- public List<AthenzDomain> getDomainListByAccount(String id) {
- log("getDomainListById()");
- return new ArrayList<>();
- }
-
- @Override
- public AthenzDomainMeta getDomainMeta(AthenzDomain domain) {
- return Optional.ofNullable(athenz.domains.get(domain))
- .map(d -> d.attributes)
- .map(attrs -> {
- if (attrs.containsKey("account")) {
- return new AthenzDomainMeta((String) attrs.get("account"), (String) attrs.get("gcpProject"), domain.getName());
- }
- return null;
- })
- .orElse(null);
- }
-
- @Override
- public void updateDomain(AthenzDomain domain, String mainKey, Map<String, Object> attributes) {
- if (!athenz.domains.containsKey(domain)) throw new IllegalStateException("Domain does not exist: " + domain.getName());
- athenz.domains.get(domain).withAttributes(attributes);
- }
-
- @Override
- public boolean hasAccess(AthenzResourceName resource, String action, AthenzIdentity identity) {
- log("hasAccess(resource=%s, action=%s, identity=%s)", resource, action, identity);
- if (resource.getDomain().equals(this.controllerIdentity.getDomain())) {
- if (isHostedOperator(identity)) {
- return true;
- }
- if (resource.getEntityName().startsWith("service.hosting.tenant.")) {
- AthenzDomain tenantDomainName = getTenantDomain(resource);
- AthenzDbMock.Domain tenantDomain = getDomainOrThrow(tenantDomainName, true);
- if (tenantDomain.admins.contains(identity) || tenantDomain.tenantAdmins.contains(identity)) {
- return true;
- }
- if (resource.getEntityName().contains(".res_group.")) {
- ApplicationId applicationName = new ApplicationId(getResourceGroupName(resource));
- AthenzDbMock.Application application = tenantDomain.applications.get(applicationName);
- if (application == null) {
- throw zmsException(400, "Application '%s' not found", applicationName);
- }
- return application.acl.get(ApplicationAction.valueOf(action)).contains(identity);
- }
- return false;
- }
- return false;
- } else {
- AthenzDbMock.Domain domain = getDomainOrThrow(resource.getDomain(), false);
- return domain.checkAccess(identity, action, resource.getEntityName());
- }
- }
-
- @Override
- public void createPolicy(AthenzDomain athenzDomain, String athenzPolicy) {
- Map<String, AthenzDbMock.Policy> policies = athenz.getOrCreateDomain(athenzDomain).policies;
- if (policies.containsKey(athenzPolicy)) {
- throw new IllegalArgumentException("Policy already exists");
- }
- policies.put(athenzPolicy, new AthenzDbMock.Policy(athenzPolicy));
- }
-
- @Override
- public void addPolicyRule(AthenzDomain athenzDomain, String athenzPolicy, String action, AthenzResourceName resourceName, AthenzRole athenzRole) {
- AthenzDbMock.Policy policy = athenz.getOrCreateDomain(athenzDomain).policies.get(athenzPolicy);
- if (policy == null) throw new IllegalArgumentException("No policy with name " + athenzPolicy);
- policy.assertions.add(new AthenzDbMock.Assertion(athenzRole.roleName(), action, resourceName.toResourceNameString()));
- }
-
- @Override
- public boolean deletePolicyRule(AthenzDomain athenzDomain, String athenzPolicy, String action, AthenzResourceName resourceName, AthenzRole athenzRole) {
- var assertion = new AthenzDbMock.Assertion(athenzRole.roleName(), action, resourceName.toResourceNameString());
- var policy = athenz.getOrCreateDomain(athenzDomain).policies.get(athenzPolicy);
- return policy.assertions.remove(assertion);
- }
-
- @Override
- public Optional<AthenzPolicy> getPolicy(AthenzDomain domain, String name) {
- AthenzDbMock.Policy policy = athenz.getOrCreateDomain(domain).policies.get(name);
- if (policy == null) return Optional.empty();
- List<AthenzAssertion> assertions = policy.assertions.stream()
- .map(a -> AthenzAssertion.newBuilder(
- new AthenzRole(domain, a.role()),
- AthenzResourceName.fromString(a.resource()),
- a.action())
- .build())
- .toList();
- return Optional.of(new AthenzPolicy(policy.name(), assertions));
- }
-
- @Override
- public Map<AthenzIdentity,String> listPendingRoleApprovals(AthenzRole athenzRole) {
- return Map.of();
- }
-
- @Override
- public void decidePendingRoleMembership(AthenzRole athenzRole, AthenzIdentity athenzIdentity, Instant expiry, Optional<String> reason, Optional<OAuthCredentials> oAuthCredentials, boolean approve) {
- }
-
- @Override
- public List<AthenzIdentity> listMembers(AthenzRole athenzRole) {
- return List.of();
- }
-
- @Override
- public List<AthenzService> listServices(AthenzDomain athenzDomain) {
- return athenz.getOrCreateDomain(athenzDomain).services.keySet().stream()
- .map(serviceName -> new AthenzService(athenzDomain, serviceName))
- .toList();
- }
-
- @Override
- public void createOrUpdateService(AthenzService athenzService) {
- athenz.getOrCreateDomain(athenzService.getDomain()).services.put(athenzService.getName(), new AthenzDbMock.Service(false));
- }
-
- @Override
- public void updateServicePublicKey(AthenzService athenzService, String publicKeyId, PublicKey publicKey) {
-
- }
-
- @Override
- public void updateProviderEndpoint(AthenzService athenzService, String endpoint) {
-
- }
-
- @Override
- public void deleteService(AthenzService athenzService) {
- athenz.getOrCreateDomain(athenzService.getDomain()).services.remove(athenzService.getName());
- }
-
- @Override
- public void createRole(AthenzRole role, Map<String, Object> properties) {
- List<AthenzDbMock.Role> roles = athenz.getOrCreateDomain(role.domain()).roles;
- if (roles.stream().anyMatch(r -> r.name().equals(role.roleName()))) {
- throw new IllegalArgumentException("Role already exists");
- }
- roles.add(new AthenzDbMock.Role(role.roleName()));
- }
-
- @Override
- public Set<AthenzRole> listRoles(AthenzDomain domain) {
- return athenz.getOrCreateDomain(domain).roles.stream()
- .map(role -> new AthenzRole(domain, role.name()))
- .collect(Collectors.toSet());
- }
-
- @Override
- public Set<String> listPolicies(AthenzDomain domain) {
- return athenz.getOrCreateDomain(domain).policies.keySet();
- }
-
- @Override
- public void deleteRole(AthenzRole athenzRole) {
- athenz.domains.get(athenzRole.domain()).roles.removeIf(role -> role.name().equals(athenzRole.roleName()));
- }
-
- @Override
- public void createSubdomain(AthenzDomain parent, String name, Map<String, Object> attributes) {
- AthenzDomain domain = new AthenzDomain(parent, name);
- if (athenz.domains.containsKey(domain)) throw new IllegalStateException("Subdomain already exists: %s".formatted(domain.getName()));
- athenz.getOrCreateDomain(domain, attributes);
- }
-
- @Override
- public AthenzRoleInformation getFullRoleInformation(AthenzRole role) {
- return new AthenzRoleInformation(role.domain(), role.roleName(), true, true, Optional.empty(), List.of());
- }
-
- @Override
- public QuotaUsage getQuotaUsage() {
- return new QuotaUsage(0.1, 0.2, 0.3, 0.4, 0.5);
- }
-
- @Override
- public void deleteSubdomain(AthenzDomain parent, String name) {
- athenz.domains.remove(new AthenzDomain(parent.getName() + "." + name));
- }
-
- @Override
- public void deletePolicy(AthenzDomain domain, String athenzPolicy) {
- athenz.getOrCreateDomain(domain).policies.remove(athenzPolicy);
- }
-
- @Override
- public void close() {}
-
- private static AthenzDomain getTenantDomain(AthenzResourceName resource) {
- Matcher matcher = TENANT_RESOURCE_PATTERN.matcher(resource.getEntityName());
- if (!matcher.matches()) {
- throw new IllegalArgumentException(resource.toResourceNameString());
- }
- return new AthenzDomain(matcher.group("tenantDomain"));
- }
-
- private static String getResourceGroupName(AthenzResourceName resource) {
- Matcher matcher = APPLICATION_RESOURCE_PATTERN.matcher(resource.getEntityName());
- if (!matcher.matches()) {
- throw new IllegalArgumentException(resource.toResourceNameString());
- }
- return matcher.group("resourceGroup");
- }
-
- private AthenzDbMock.Domain getDomainOrThrow(AthenzDomain domainName, boolean verifyVespaTenant) {
- AthenzDbMock.Domain domain = Optional.ofNullable(athenz.domains.get(domainName))
- .orElseThrow(() -> zmsException(400, "Domain '%s' not found", domainName));
- if (verifyVespaTenant && !domain.isVespaTenant) {
- throw zmsException(400, "Domain not a Vespa tenant: '%s'", domainName);
- }
- return domain;
- }
-
- private boolean isHostedOperator(AthenzIdentity identity) {
- return athenz.hostedOperators.contains(identity);
- }
-
- private static ZmsClientException zmsException(int code, String message, Object... args) {
- return new ZmsClientException(code, String.format(message, args));
- }
-
- private static void log(String format, Object... args) {
- log.log(Level.FINE, String.format(format, args));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZtsClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZtsClientMock.java
deleted file mode 100644
index a6e54f17507..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/ZtsClientMock.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.security.Pkcs10Csr;
-import com.yahoo.vespa.athenz.api.AthenzAccessToken;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzResourceName;
-import com.yahoo.vespa.athenz.api.AthenzRole;
-import com.yahoo.vespa.athenz.api.AwsRole;
-import com.yahoo.vespa.athenz.api.AwsTemporaryCredentials;
-import com.yahoo.vespa.athenz.api.ZToken;
-import com.yahoo.vespa.athenz.client.zms.ZmsClient;
-import com.yahoo.vespa.athenz.client.zts.Identity;
-import com.yahoo.vespa.athenz.client.zts.InstanceIdentity;
-import com.yahoo.vespa.athenz.client.zts.ZtsClient;
-
-import java.security.KeyPair;
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.util.List;
-import java.util.Optional;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * @author bjorncs
- */
-public class ZtsClientMock implements ZtsClient {
- private static final Logger log = Logger.getLogger(ZtsClientMock.class.getName());
-
- private final AthenzDbMock athenz;
- private final Optional<ZmsClient> zmsClient;
-
- public ZtsClientMock(AthenzDbMock athenz) {
- this(athenz, null);
- }
- public ZtsClientMock(AthenzDbMock athenz, ZmsClient zmsClient) {
- this.athenz = athenz;
- this.zmsClient = Optional.ofNullable(zmsClient);
- }
-
- @Override
- public List<AthenzDomain> getTenantDomains(AthenzIdentity providerIdentity, AthenzIdentity userIdentity, String roleName) {
- log.log(Level.FINE, String.format("getTenantDomains(providerIdentity='%s', userIdentity='%s', roleName='%s')",
- providerIdentity.getFullName(), userIdentity.getFullName(), roleName));
- return athenz.domains.values().stream()
- .filter(domain -> domain.tenantAdmins.contains(userIdentity) || domain.admins.contains(userIdentity))
- .map(domain -> domain.name)
- .toList();
- }
-
- @Override
- public InstanceIdentity registerInstance(AthenzIdentity providerIdentity, AthenzIdentity instanceIdentity, String attestationData, Pkcs10Csr csr) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public InstanceIdentity refreshInstance(AthenzIdentity providerIdentity, AthenzIdentity instanceIdentity, String instanceId, Pkcs10Csr csr) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Identity getServiceIdentity(AthenzIdentity identity, String keyId, Pkcs10Csr csr) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Identity getServiceIdentity(AthenzIdentity identity, String keyId, KeyPair keyPair, String dnsSuffix) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public ZToken getRoleToken(AthenzDomain domain, Duration expiry) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public ZToken getRoleToken(AthenzRole athenzRole, Duration expiry) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public AthenzAccessToken getAccessToken(AthenzDomain domain, List<AthenzIdentity> proxyPrincipals) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public AthenzAccessToken getAccessToken(List<AthenzRole> athenzRole) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public X509Certificate getRoleCertificate(AthenzRole role, Pkcs10Csr csr, Duration expiry) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public X509Certificate getRoleCertificate(AthenzRole role, Pkcs10Csr csr) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public AwsTemporaryCredentials getAwsTemporaryCredentials(AthenzDomain athenzDomain, AwsRole awsRole, Duration duration, String externalId) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean hasAccess(AthenzResourceName resource, String action, AthenzIdentity identity) {
- return zmsClient.orElseThrow(UnsupportedOperationException::new)
- .hasAccess(resource, action, identity);
- }
-
- @Override
- public void close() {
-
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/package-info.java
deleted file mode 100644
index a37f90d921e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/athenz/package-info.java
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author bjorncs
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.athenz;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/EnclaveAccessService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/EnclaveAccessService.java
deleted file mode 100644
index 361898ce057..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/EnclaveAccessService.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.aws;
-
-import com.yahoo.config.provision.CloudAccount;
-
-import java.util.Set;
-
-/**
- * @author jonmv
- */
-public interface EnclaveAccessService {
-
- /**
- * Ensures the given enclave accounts have access to resources they require to function.
- * @return the degree to which the run was successful - a number between 0 (no success), to 1 (complete success)
- */
- double allowAccessFor(Set<CloudAccount> accounts);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockEnclaveAccessService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockEnclaveAccessService.java
deleted file mode 100644
index c9c552d8b8d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockEnclaveAccessService.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.aws;
-
-import com.yahoo.config.provision.CloudAccount;
-
-import java.util.Set;
-import java.util.TreeSet;
-
-/**
- * @author jonmv
- */
-public class MockEnclaveAccessService implements EnclaveAccessService {
-
- private volatile Set<CloudAccount> currentAccounts = new TreeSet<>();
-
- public Set<CloudAccount> currentAccounts() { return currentAccounts; }
-
- @Override
- public double allowAccessFor(Set<CloudAccount> accounts) {
- currentAccounts = new TreeSet<>(accounts);
- return 1;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockResourceTagger.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockResourceTagger.java
deleted file mode 100644
index d67a97e4f29..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockResourceTagger.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.aws;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * @author olaa
- */
-public class MockResourceTagger implements ResourceTagger {
-
- Map<ZoneId, Map<HostName, ApplicationId>> values = new HashMap<>();
-
- @Override
- public int tagResources(ZoneApi zone, Map<HostName, ApplicationId> ownerOfHosts) {
- values.put(zone.getId(), ownerOfHosts);
- return 0;
- }
-
- public Map<ZoneId, Map<HostName, ApplicationId>> getValues() {
- return values;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockRoleService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockRoleService.java
deleted file mode 100644
index c85591733fe..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockRoleService.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.api.integration.aws;
-
-import com.yahoo.config.provision.TenantName;
-
-import java.util.List;
-
-public class MockRoleService extends NoopRoleService {
-
- private List<TenantName> maintainedTenants;
-
- @Override
- public double maintainRoles(List<TenantName> tenants) {
- maintainedTenants = List.copyOf(tenants);
- return 1;
- }
-
- public List<TenantName> maintainedTenants() {
- return maintainedTenants;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/NoopRoleService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/NoopRoleService.java
deleted file mode 100644
index 8f391e70d36..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/NoopRoleService.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.aws;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.util.List;
-import java.util.Optional;
-
-/**
- * @author mortent
- */
-public class NoopRoleService implements RoleService {
-
- @Override
- public Optional<TenantRoles> createTenantRole(Tenant tenant) {
- return Optional.empty();
- }
-
- @Override
- public TenantRoles getTenantRole(TenantName tenant) {
- return new TenantRoles(tenant.value() + "-host-role", tenant.value() + "-host-service-role", tenant.value() + "-tenant-role");
- }
-
- @Override
- public void deleteTenantRole(TenantName tenant) { }
-
- @Override
- public String createTenantPolicy(TenantName tenant, String policyName, String awsId, String role) {
- return "";
- }
-
- @Override
- public void deleteTenantPolicy(TenantName tenant, String policyName, String role) { }
-
- @Override
- public double maintainRoles(List<TenantName> tenants) { return 1; }
-
- @Override
- public void cleanupRoles(List<TenantName> tenants) {
-
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/ResourceTagger.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/ResourceTagger.java
deleted file mode 100644
index adcb419dc5c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/ResourceTagger.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.aws;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneApi;
-
-import java.util.Map;
-
-/**
- * @author olaa
- */
-public interface ResourceTagger {
-
- ApplicationId INFRASTRUCTURE_APPLICATION = ApplicationId.from("hosted-vespa", "infrastructure", "default");
-
-
- /**
- * Returns number of tagged resources
- */
- int tagResources(ZoneApi zone, Map<HostName, ApplicationId> ownerOfHosts);
-
- static ResourceTagger empty() {
- return (zone, tenantOfHosts) -> 0;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/RoleService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/RoleService.java
deleted file mode 100644
index 5e53e659af2..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/RoleService.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.aws;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.util.List;
-import java.util.Optional;
-
-/**
- * @author mortent
- */
-public interface RoleService {
-
- Optional<TenantRoles> createTenantRole(Tenant tenant);
-
- /** Retrieve the names of the tenant roles (host and container). Does not guarantee these roles exist */
- TenantRoles getTenantRole(TenantName tenant);
-
- void deleteTenantRole(TenantName tenant);
-
- String createTenantPolicy(TenantName tenant, String policyName, String awsId, String role);
-
- void deleteTenantPolicy(TenantName tenant, String policyName, String role);
-
- /*
- * Maintain roles for the tenants in the system. Create missing roles, update trust.
- */
- double maintainRoles(List<TenantName> tenants);
-
- void cleanupRoles(List<TenantName> deletedTenants);
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/TenantRoles.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/TenantRoles.java
deleted file mode 100644
index 028310d44d7..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/TenantRoles.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.aws;
-
-/**
- * @author mortent
- */
-public class TenantRoles {
- private final String tenantHostRole;
- private final String tenantHostServiceRole;
- private final String containerRole;
-
- public TenantRoles(String tenantHostRole, String tenantHostServiceRole, String containerRole) {
- this.tenantHostRole = tenantHostRole;
- this.tenantHostServiceRole = tenantHostServiceRole;
- this.containerRole = containerRole;
- }
-
- public String tenantHostRole() {
- return tenantHostRole;
- }
-
- public String hostServiceRole() {
- return tenantHostServiceRole;
- }
-
- public String containerRole() {
- return containerRole;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/package-info.java
deleted file mode 100644
index e12912a7086..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.aws;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/AcceptedCountries.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/AcceptedCountries.java
deleted file mode 100644
index 931251226f9..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/AcceptedCountries.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import java.util.List;
-
-/**
- * @author bjorncs
- */
-public record AcceptedCountries(List<Country> countries) {
-
- public AcceptedCountries {
- countries = List.copyOf(countries);
- }
-
- public record Country(String code, String displayName, boolean taxIdMandatory, List<TaxType> taxTypes) {
- public Country {
- taxTypes = List.copyOf(taxTypes);
- }
- }
-
- public record TaxType(String id, String description, String pattern, String example) {}
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java
deleted file mode 100644
index 7c95a2138d2..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Bill.java
+++ /dev/null
@@ -1,488 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.time.ZonedDateTime;
-import java.util.Collection;
-import java.util.EnumMap;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.UUID;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-
-/**
- * An Bill is an identifier with a status (with history) and line items. A line item is the meat and
- * potatoes of the content of the bill, and are a history of items. Most line items are connected to
- * a given deployment in Vespa Cloud, but they can also be manually added to e.g. give a discount or represent
- * support.
- * <p>
- * All line items have a Plan associated with them - which was used to map from utilization to an actual price.
- * <p>
- * The bill has a status history, but only the latest status is exposed through this API.
- *
- * @author ogronnesby
- */
-public class Bill {
- private static final BigDecimal SCALED_ZERO = new BigDecimal("0.00");
-
- private final Id id;
- private final TenantName tenant;
- private final List<LineItem> lineItems;
- private final StatusHistory statusHistory;
- private final ZonedDateTime startTime;
- private final ZonedDateTime endTime;
- private final String exportedId;
-
- public Bill(Id id, TenantName tenant, StatusHistory statusHistory, List<LineItem> lineItems,
- ZonedDateTime startTime, ZonedDateTime endTime) {
- this(id, tenant, statusHistory, lineItems, startTime, endTime, null);
- }
-
- public Bill(Id id, TenantName tenant, StatusHistory statusHistory, List<LineItem> lineItems,
- ZonedDateTime startTime, ZonedDateTime endTime, String exportedId) {
- this.id = id;
- this.tenant = tenant;
- this.lineItems = List.copyOf(lineItems);
- this.statusHistory = statusHistory;
- this.startTime = startTime;
- this.endTime = endTime;
- this.exportedId = exportedId;
- }
-
- public Id id() {
- return id;
- }
-
- public TenantName tenant() {
- return tenant;
- }
-
- public BillStatus status() {
- return statusHistory.current();
- }
-
- public StatusHistory statusHistory() {
- return statusHistory;
- }
-
- public List<LineItem> lineItems() {
- return lineItems;
- }
-
- public ZonedDateTime getStartTime() {
- return startTime;
- }
-
- public ZonedDateTime getEndTime() {
- return endTime;
- }
-
- public Optional<String> getExportedId() {
- return Optional.ofNullable(exportedId);
- }
-
- public LocalDate getStartDate() {
- return startTime.toLocalDate();
- }
-
- public LocalDate getEndDate() {
- return endTime.minusDays(1).toLocalDate();
- }
-
- public BigDecimal sum() {
- return lineItems.stream().map(LineItem::amount).reduce(SCALED_ZERO, BigDecimal::add);
- }
-
- public BigDecimal sumCpuHours() {
- return sumResourceValues(LineItem::getCpuHours);
- }
-
- public BigDecimal sumMemoryHours() {
- return sumResourceValues(LineItem::getMemoryHours);
- }
-
- public BigDecimal sumDiskHours() {
- return sumResourceValues(LineItem::getDiskHours);
- }
-
- public BigDecimal sumCpuCost() {
- return sumResourceValues(LineItem::getCpuCost);
- }
-
- public BigDecimal sumMemoryCost() {
- return sumResourceValues(LineItem::getMemoryCost);
- }
-
- public BigDecimal sumDiskCost() {
- return sumResourceValues(LineItem::getDiskCost);
- }
-
- public BigDecimal sumGpuCost() {
- return sumResourceValues(LineItem::getGpuCost);
- }
-
- public BigDecimal sumAdditionalCost() {
- // anything that is not covered by the cost for resources is "additional" costs
- var resourceCosts = sumCpuCost().add(sumMemoryCost()).add(sumDiskCost()).add(sumGpuCost());
- return sum().subtract(resourceCosts);
- }
-
- private BigDecimal sumResourceValues(Function<LineItem, Optional<BigDecimal>> f) {
- return lineItems.stream().flatMap(li -> f.apply(li).stream()).reduce(SCALED_ZERO, BigDecimal::add);
- }
-
- public static final class Id {
- private final String value;
-
- public static Id of(String value) {
- Objects.requireNonNull(value);
- return new Id(value);
- }
-
- public static Id generate() {
- var id = UUID.randomUUID().toString();
- return new Id(id);
- }
-
- private Id(String value) {
- this.value = value;
- }
-
- public String value() {
- return value;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Id billId = (Id) o;
- return value.equals(billId.value);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(value);
- }
-
- @Override
- public String toString() {
- return "BillId{" +
- "value='" + value + '\'' +
- '}';
- }
- }
-
- /**
- * Represents a chargeable line on a bill.
- */
- public static class LineItem {
- private final String id;
- private final String description;
- private final BigDecimal amount;
- private final String plan;
- private final String agent;
- private final ZonedDateTime addedAt;
- private ZonedDateTime startedAt;
- private ZonedDateTime endedAt;
- private ApplicationId applicationId;
- private ZoneId zoneId;
- private BigDecimal cpuHours;
- private BigDecimal memoryHours;
- private BigDecimal diskHours;
- private BigDecimal gpuHours;
- private BigDecimal cpuCost;
- private BigDecimal memoryCost;
- private BigDecimal diskCost;
- private BigDecimal gpuCost;
- private NodeResources.Architecture architecture;
- private int majorVersion;
- private CloudAccount cloudAccount;
- private String exportedId;
-
- public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt) {
- this.id = id;
- this.description = description;
- this.amount = amount;
- this.plan = plan;
- this.agent = agent;
- this.addedAt = addedAt;
- this.cloudAccount = CloudAccount.empty;
- }
-
- public LineItem(String id, String description, BigDecimal amount, String plan, String agent, ZonedDateTime addedAt,
- ZonedDateTime startedAt, ZonedDateTime endedAt, ApplicationId applicationId, ZoneId zoneId,
- BigDecimal cpuHours, BigDecimal memoryHours, BigDecimal diskHours, BigDecimal gpuHours, BigDecimal cpuCost,
- BigDecimal memoryCost, BigDecimal diskCost, BigDecimal gpuCost, NodeResources.Architecture architecture,
- int majorVersion, CloudAccount cloudAccount, String exportedId)
- {
- this(id, description, amount, plan, agent, addedAt);
- this.startedAt = startedAt;
- this.endedAt = endedAt;
-
- if (applicationId == null && zoneId != null)
- throw new IllegalArgumentException("Must supply applicationId if zoneId is supplied");
-
- this.applicationId = applicationId;
- this.zoneId = zoneId;
- this.cpuHours = cpuHours;
- this.memoryHours = memoryHours;
- this.diskHours = diskHours;
- this.gpuHours = gpuHours;
- this.cpuCost = cpuCost;
- this.memoryCost = memoryCost;
- this.diskCost = diskCost;
- this.architecture = architecture;
- this.majorVersion = majorVersion;
- this.gpuCost = gpuCost;
- this.cloudAccount = cloudAccount;
- this.exportedId = exportedId;
- }
-
- /** The opaque ID of this */
- public String id() {
- return id;
- }
-
- /** The string description of this - used for display purposes */
- public String description() {
- return description;
- }
-
- /** The dollar amount of this */
- public BigDecimal amount() {
- return SCALED_ZERO.add(amount);
- }
-
- /** The plan used to calculate amount of this */
- public String plan() {
- return plan;
- }
-
- /** Who created this line item */
- public String agent() {
- return agent;
- }
-
- /** When was this line item added */
- public ZonedDateTime addedAt() {
- return addedAt;
- }
-
- /** What time period is this line item for - time start */
- public Optional<ZonedDateTime> startedAt() {
- return Optional.ofNullable(startedAt);
- }
-
- /** What time period is this line item for - time end */
- public Optional<ZonedDateTime> endedAt() {
- return Optional.ofNullable(endedAt);
- }
-
- /** Optionally - what application is this line item about */
- public Optional<ApplicationId> applicationId() {
- return Optional.ofNullable(applicationId);
- }
-
- /** Optionally - what zone deployment is this line item about */
- public Optional<ZoneId> zoneId() {
- return Optional.ofNullable(zoneId);
- }
-
- public Optional<BigDecimal> getCpuHours() {
- return Optional.ofNullable(cpuHours);
- }
-
- public Optional<BigDecimal> getMemoryHours() {
- return Optional.ofNullable(memoryHours);
- }
-
- public Optional<BigDecimal> getDiskHours() {
- return Optional.ofNullable(diskHours);
- }
-
- public Optional<BigDecimal> getGpuHours() {
- return Optional.ofNullable(gpuHours);
- }
-
- public Optional<BigDecimal> getCpuCost() {
- return Optional.ofNullable(cpuCost);
- }
-
- public Optional<BigDecimal> getMemoryCost() {
- return Optional.ofNullable(memoryCost);
- }
-
- public Optional<BigDecimal> getDiskCost() {
- return Optional.ofNullable(diskCost);
- }
-
- public Optional<BigDecimal> getGpuCost() {
- return Optional.ofNullable(gpuCost);
- }
-
- public Optional<NodeResources.Architecture> getArchitecture() {
- return Optional.ofNullable(architecture);
- }
-
- public int getMajorVersion() {
- return majorVersion;
- }
-
- public CloudAccount getCloudAccount() {
- return cloudAccount;
- }
-
- public Optional<String> getExportedId() {
- return Optional.ofNullable(exportedId);
- }
-
- public boolean isAdditional() {
- return cpuCost != null && diskCost != null && memoryCost != null && gpuCost != null;
- }
-
- public boolean isResource() {
- return ! isAdditional();
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- LineItem lineItem = (LineItem) o;
- return id.equals(lineItem.id) &&
- description.equals(lineItem.description) &&
- amount.equals(lineItem.amount) &&
- plan.equals(lineItem.plan) &&
- agent.equals(lineItem.agent) &&
- addedAt.equals(lineItem.addedAt) &&
- startedAt.equals(lineItem.startedAt) &&
- endedAt.equals(lineItem.endedAt) &&
- applicationId.equals(lineItem.applicationId) &&
- zoneId.equals(lineItem.zoneId) &&
- majorVersion == lineItem.majorVersion;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id, description, amount, plan, agent, addedAt, startedAt, endedAt, applicationId, zoneId, majorVersion);
- }
-
- @Override
- public String toString() {
- return "LineItem{" +
- "id='" + id + '\'' +
- ", description='" + description + '\'' +
- ", amount=" + amount +
- ", plan='" + plan + '\'' +
- ", agent='" + agent + '\'' +
- ", addedAt=" + addedAt +
- ", startedAt=" + startedAt +
- ", endedAt=" + endedAt +
- ", applicationId=" + applicationId +
- ", zoneId=" + zoneId +
- '}';
- }
- }
-
- public enum ItemKeyType {
- plan(LineItem::plan),
- version(LineItem::getMajorVersion),
- account(item -> {
- var account = item.getCloudAccount();
- return account.isUnspecified() ? null : account;
- }),
- architecture(item -> {
- var arch = item.getArchitecture();
- return arch.orElse(null);
- }),
- application(item -> {
- var app = item.applicationId();
- return app.orElse(null);
- }),
- environment(item -> {
- var zone = item.zoneId();
- return zone.map(ZoneId::environment).orElse(null);
- });
-
- private final Function<LineItem, Object> extractor;
-
- ItemKeyType(Function<LineItem, Object> extractor) {
- this.extractor = extractor;
- }
-
- public Function<LineItem, Object> extractor() {
- return extractor;
- }
- }
-
- public record ItemKey(EnumMap<ItemKeyType, Object> keys) {}
-
- public record ItemRequest(TreeSet<ItemKeyType> keyTypes) {
- public static ItemRequest of(Collection<ItemKeyType> keyTypes) {
- return new ItemRequest(new TreeSet<>(keyTypes));
- }
- }
- public record ItemSummary(
- BigDecimal cpuUsage,
- BigDecimal ramUsage,
- BigDecimal diskUsage,
- BigDecimal gpuUsage,
- BigDecimal cpuCost,
- BigDecimal ramCost,
- BigDecimal diskCost,
- BigDecimal gpuCost) {
-
- static ItemSummary from(List<LineItem> items) {
- return new ItemSummary(
- sum(items, LineItem::getCpuHours),
- sum(items, LineItem::getMemoryHours),
- sum(items, LineItem::getDiskHours),
- sum(items, LineItem::getGpuHours),
- sum(items, LineItem::getCpuCost),
- sum(items, LineItem::getMemoryCost),
- sum(items, LineItem::getDiskCost),
- sum(items, LineItem::getGpuCost));
- }
-
- private static BigDecimal sum(List<LineItem> items, Function<LineItem, Optional<BigDecimal>> mapper) {
- return items.stream().map(mapper).map(o -> o.orElse(BigDecimal.ZERO)).reduce(BigDecimal.ZERO, BigDecimal::add);
- }
- }
-
- public Map<ItemKey, ItemSummary> summarizeBy(ItemRequest request) {
- var itemsByKey = this.lineItems.stream()
- .filter(LineItem::isResource)
- .collect(
- Collectors.groupingBy(
- (LineItem item) -> createKeyFromItem(request, item),
- Collectors.toList()));
-
- return itemsByKey.entrySet().stream()
- .map(item -> Map.entry(item.getKey(), ItemSummary.from(item.getValue())))
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
- }
-
- private static ItemKey createKeyFromItem(ItemRequest request, LineItem item) {
- var key = new EnumMap<>(ItemKeyType.class);
- for (var keyType : request.keyTypes()) {
- key.put(keyType, keyType.extractor().apply(item));
- }
- return new ItemKey(key);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatus.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatus.java
deleted file mode 100644
index 17698aff6f4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatus.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import java.util.Arrays;
-
-/**
- * @author gjoranv
- */
-public enum BillStatus {
- OPEN, // All bills start in this state. The bill can be modified and exported/synced to external systems.
- FROZEN, // Syncing to external systems is switched off. No changes can be made.
- SUCCESSFUL, // Final state for a valid bill.
- VOID; // Final state, indicating that the bill is not valid.
-
- // Legacy states, used by historical bills
- private static final String LEGACY_ISSUED = "ISSUED";
- private static final String LEGACY_EXPORTED = "EXPORTED";
- private static final String LEGACY_CANCELED = "CANCELED";
-
- private final String value;
-
- BillStatus() {
- this.value = name();
- }
-
- public String value() {
- return value;
- }
-
- /**
- * Returns true if the bill is in a final state.
- */
- public boolean isFinal() {
- return this == SUCCESSFUL || this == VOID;
- }
-
- public static BillStatus from(String status) {
- if (LEGACY_ISSUED.equals(status) || LEGACY_EXPORTED.equals(status)) return OPEN;
- if (LEGACY_CANCELED.equals(status)) return VOID;
-
- return Arrays.stream(values())
- .filter(s -> s.value.equals(status))
- .findFirst()
- .orElseThrow(() -> new IllegalArgumentException("Unknown bill status: " + status));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
deleted file mode 100644
index dcd1a057f49..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingController.java
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.tenant.TaxId;
-
-import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.time.ZonedDateTime;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * A service that controls creation of bills based on the resource usage of a tenant, controls the quota for a
- * tenant, and controls the plan the tenant is on.
- *
- * @author ogronnesby
- * @author olaa
- */
-public interface BillingController {
-
- /**
- * Get the plan ID for the given tenant.
- * This method will not fail if the tenant does not exist, it will return the default plan for that tenant instead.
- */
- PlanId getPlan(TenantName tenant);
-
- /**
- * Return the list of tenants with the given plan.
- * @param existing All existing tenants in the system
- * @param planId The ID of the plan to filter existing tenants on.
- * @return The tenants that have the given plan.
- */
- List<TenantName> tenantsWithPlan(List<TenantName> existing, PlanId planId);
-
- /**
- * The quota for the given tenant.
- * This method will return default quota for tenants that do not exist.
- */
- Quota getQuota(TenantName tenant);
-
- /**
- * Set the plan for the current tenant. Checks some pre-conditions to see if the tenant is eligible for the
- * given plan.
- * @param tenant The name of the tenant.
- * @param planId The ID of the plan to change to.
- * @param hasDeployments Does the tenant have active deployments.
- * @param isAccountant Is it the hosted accountant that is doing the operation
- * @return String containing error message if something went wrong. Empty otherwise
- */
- PlanResult setPlan(TenantName tenant, PlanId planId, boolean hasDeployments, boolean isAccountant);
-
- /**
- * Create a bill of unbilled use for the given tenant in the given time period.
- * @param tenant The name of the tenant.
- * @param startTime The start of the billing period
- * @param endTime The end of the billing period
- * @param agent The agent that creates the bill
- * @return The ID of the new bill.
- */
- Bill.Id createBillForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent);
-
- /**
- * Create an unpersisted bill of unbilled use for the given tenant from the end of last bill until the given date.
- * This is used to show "unbilled use" in the Console.
- * @param tenant The name of the tenant.
- * @param until The end date of the unbilled use period.
- * @return A bill with the resource use and cost.
- */
- Bill createUncommittedBill(TenantName tenant, LocalDate until);
-
- /** Run {createUncommittedBill} for all tenants with unbilled use */
- Map<TenantName, Bill> createUncommittedBills(LocalDate until);
-
- /** Get line items that have been manually added to a tenant, but is not yet part of a bill */
- List<Bill.LineItem> getUnusedLineItems(TenantName tenant);
-
- /** Add a line item to the given bill */
- void addLineItem(TenantName tenant, String description, BigDecimal amount, Optional<Bill.Id> billId, String agent);
-
- /** Delete a line item - only available for unused line items */
- void deleteLineItem(String lineItemId);
-
- /** Get all bills for the given tenant */
- List<Bill> getBillsForTenant(TenantName tenant);
-
- /** Get the bill with the given id */
- Bill getBill(Bill.Id billId);
-
- /** Get the bill collection method for the given tenant */
- default CollectionMethod getCollectionMethod(TenantName tenant) {
- return CollectionMethod.NONE;
- }
-
- /** Set the bill collection method for the given tenant */
- default CollectionResult setCollectionMethod(TenantName tenant, CollectionMethod method) {
- return CollectionResult.error("Method not implemented");
- }
-
- /** Test if the number of tenants with the given plan is under the given limit */
- default boolean tenantsWithPlanUnderLimit(Plan plan, int limit) {
- return true;
- }
-
- default void updateCache(List<TenantName> tenants) {}
-
- /** Get the list of countries that are accepted */
- AcceptedCountries getAcceptedCountries();
-
- /** Validation of tax id */
- void validateTaxId(TaxId id) throws IllegalArgumentException;
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java
deleted file mode 100644
index c5859cd7d2f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClient.java
+++ /dev/null
@@ -1,158 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.config.provision.TenantName;
-
-import java.time.ZonedDateTime;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * Interface that talks about bills in the billing API. It is a layer on top of the SQL
- * database where we store data about bills.
- *
- * @author olaa
- * @author ogronnesby
- */
-public interface BillingDatabaseClient {
-
- boolean setActivePaymentInstrument(InstrumentOwner paymentInstrument);
-
- Optional<InstrumentOwner> getDefaultPaymentInstrumentForTenant(TenantName from);
-
- /**
- * Create a completely new Bill in the open state with no LineItems.
- *
- * @param tenant The name of the tenant the bill is for
- * @param agent The agent that created the bill
- * @return The Id of the new bill
- */
- Bill.Id createBill(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent);
-
- /**
- * Read the given bill from the data source
- *
- * @param billId The Id of the bill to retrieve
- * @return The Bill if it exists, Optional.empty() if not.
- */
- Optional<Bill> readBill(Bill.Id billId);
-
- /**
- * Get all bills for a given tenant, ordered by date
- *
- * @param tenant The name of the tenant
- * @return List of all bills ordered by date
- */
- List<Bill> readBillsForTenant(TenantName tenant);
-
- /**
- * Read all bills, ordered by date
- * @return List of all bills ordered by date
- */
- List<Bill> readBills();
-
- /**
- * Add a line item to an open bill
- *
- * @param lineItem The line item to add
- * @param billId The optional ID of the bill this line item is for
- * @return The Id of the new line item
- * @throws RuntimeException if the bill is not in OPEN state
- */
- String addLineItem(TenantName tenantName, Bill.LineItem lineItem, Optional<Bill.Id> billId);
-
- /**
- * Set status for the given bill
- *
- * @param billId The ID of the bill this status is for
- * @param agent The agent that added the status
- * @param status The new status of the bill
- */
- void setStatus(Bill.Id billId, String agent, BillStatus status);
-
- List<Bill.LineItem> getUnusedLineItems(TenantName tenantName);
-
- /**
- * Delete a line item
- * This is only allowed if the line item has not yet been associated with an bill
- *
- * @param lineItemId The ID of the line item
- * @throws RuntimeException if the line item is associated with an bill
- */
- void deleteLineItem(String lineItemId);
-
- /**
- * Associate all uncommitted line items to a given bill
- * This is only allowed if the line item has not already been associated with an bill
- *
- * @param tenantName The tenant we want to commit line items for
- * @param billId The ID of the line item
- * @throws RuntimeException if the line item is already associated with an bill
- */
- void commitLineItems(TenantName tenantName, Bill.Id billId);
-
- /**
- * Return the plan for the given tenant
- *
- * @param tenantName The tenant to retrieve the plan for
- * @return Optional.of the plan if present in DN, else Optional.empty
- */
- Optional<Plan> getPlan(TenantName tenantName);
-
- /**
- * Return the plan for the given tenants if present.
- * If the database does not know of the tenant, the tenant is not included in the result.
- */
- Map<TenantName, Optional<Plan>> getPlans(List<TenantName> tenants);
-
- /**
- * Returns a map with the count of plan usage. Plans that are not in use will not appear in this result.
- */
- default Map<Plan, Long> getPlanCount(List<TenantName> tenants, Plan defaultPlan) {
- return Map.of();
- }
-
- /**
- * Set the current plan for the given tenant
- *
- * @param tenantName The tenant to set the plan for
- * @param plan The plan to use
- */
- void setPlan(TenantName tenantName, Plan plan);
-
- /**
- * Deactivates the default payment instrument for a tenant, if it exists.
- * Used during tenant deletion
- */
- void deactivateDefaultPaymentInstrument(TenantName tenant);
-
- /**
- * Get the current collection method for the tenant - if one has persisted
- * @return Optional.empty if no collection method has been persisted for the tenant
- */
- Optional<CollectionMethod> getCollectionMethod(TenantName tenantName);
-
- /**
- * Set the collection method for the tenant
- * @param tenantName The name of the tenant to set collection method for
- * @param collectionMethod The collection method for the tenant
- */
- void setCollectionMethod(TenantName tenantName, CollectionMethod collectionMethod);
-
- /**
- * Performs necessary maintenance operations
- */
- void maintain();
-
- /**
- * Set the invoice id from an external system for the given bill
- */
- void setExportedInvoiceId(Bill.Id billId, String invoiceId);
-
- /**
- * Set the invoice item id from an external system for the given line item
- */
- void setExportedInvoiceItemId(String lineItemId, String invoiceItemId);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java
deleted file mode 100644
index a6bcc9bf0ed..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingDatabaseClientMock.java
+++ /dev/null
@@ -1,195 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.config.provision.TenantName;
-
-import java.time.Clock;
-import java.time.LocalDate;
-import java.time.ZoneId;
-import java.time.ZonedDateTime;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-import java.util.stream.Collectors;
-
-/**
- * @author olaa
- */
-public class BillingDatabaseClientMock implements BillingDatabaseClient {
- private final Clock clock;
- private final PlanRegistry planRegistry;
- private final Map<TenantName, Plan> tenantPlans = new HashMap<>();
- private final Map<Bill.Id, TenantName> invoices = new HashMap<>();
- private final Map<Bill.Id, List<Bill.LineItem>> lineItems = new HashMap<>();
- private final Map<TenantName, List<Bill.LineItem>> uncommittedLineItems = new HashMap<>();
-
- private final Map<Bill.Id, StatusHistory> statuses = new HashMap<>();
- private final Map<Bill.Id, ZonedDateTime> startTimes = new HashMap<>();
- private final Map<Bill.Id, ZonedDateTime> endTimes = new HashMap<>();
- private final Map<Bill.Id, String> exportedInvoiceIds = new HashMap<>();
-
- private final ZonedDateTime startTime = LocalDate.of(2020, 4, 1).atStartOfDay(ZoneId.of("UTC"));
- private final ZonedDateTime endTime = LocalDate.of(2020, 5, 1).atStartOfDay(ZoneId.of("UTC"));
-
- private final List<InstrumentOwner> paymentInstruments = new ArrayList<>();
- private final Map<TenantName, CollectionMethod> collectionMethods = new HashMap<>();
-
- public BillingDatabaseClientMock(Clock clock, PlanRegistry planRegistry) {
- this.clock = clock;
- this.planRegistry = planRegistry;
- }
-
- @Override
- public boolean setActivePaymentInstrument(InstrumentOwner paymentInstrument) {
- return paymentInstruments.add(paymentInstrument);
- }
-
- @Override
- public Optional<InstrumentOwner> getDefaultPaymentInstrumentForTenant(TenantName tenantName) {
- return paymentInstruments.stream()
- .filter(paymentInstrument -> paymentInstrument.getTenantName().equals(tenantName))
- .findFirst();
- }
-
- public BillStatus getStatus(Bill.Id invoiceId) {
- return statuses.get(invoiceId).current();
- }
-
- @Override
- public Bill.Id createBill(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent) {
- var invoiceId = Bill.Id.generate();
- invoices.put(invoiceId, tenant);
- statuses.computeIfAbsent(invoiceId, l -> StatusHistory.open(clock));
- startTimes.put(invoiceId, startTime);
- endTimes.put(invoiceId, endTime);
- return invoiceId;
- }
-
- @Override
- public Optional<Bill> readBill(Bill.Id billId) {
- var invoice = Optional.ofNullable(invoices.get(billId));
- var lines = lineItems.getOrDefault(billId, List.of());
- var status = statuses.getOrDefault(billId, StatusHistory.open(clock));
- var start = startTimes.getOrDefault(billId, startTime);
- var end = endTimes.getOrDefault(billId, endTime);
- var exportedId = exportedInvoiceId(billId);
- return invoice.map(tenant -> new Bill(billId, tenant, status, lines, start, end, exportedId));
- }
-
- @Override
- public String addLineItem(TenantName tenantName, Bill.LineItem lineItem, Optional<Bill.Id> invoiceId) {
- var lineItemId = UUID.randomUUID().toString();
- invoiceId.ifPresentOrElse(
- invoice -> lineItems.computeIfAbsent(invoice, l -> new ArrayList<>()).add(lineItem),
- () -> uncommittedLineItems.computeIfAbsent(tenantName, l -> new ArrayList<>()).add(lineItem)
- );
- return lineItemId;
- }
-
- @Override
- public void setStatus(Bill.Id invoiceId, String agent, BillStatus status) {
- statuses.computeIfAbsent(invoiceId, k -> StatusHistory.open(clock))
- .getHistory()
- .put(ZonedDateTime.now(), status);
- }
-
- @Override
- public List<Bill.LineItem> getUnusedLineItems(TenantName tenantName) {
- return uncommittedLineItems.getOrDefault(tenantName, new ArrayList<>());
- }
-
- @Override
- public void deleteLineItem(String lineItemId) {
- uncommittedLineItems.values()
- .forEach(list ->
- list.removeIf(lineItem -> lineItem.id().equals(lineItemId))
- );
- }
-
- @Override
- public void commitLineItems(TenantName tenantName, Bill.Id invoiceId) {
-
- }
-
- @Override
- public Optional<Plan> getPlan(TenantName tenantName) {
- return Optional.ofNullable(tenantPlans.get(tenantName));
- }
-
- @Override
- public Map<TenantName, Optional<Plan>> getPlans(List<TenantName> tenants) {
- return tenantPlans.entrySet().stream()
- .filter(entry -> tenants.contains(entry.getKey()))
- .collect(Collectors.toMap(
- entry -> entry.getKey(),
- entry -> planRegistry.plan(entry.getValue().id())
- ));
- }
-
- @Override
- public void setPlan(TenantName tenantName, Plan plan) {
- tenantPlans.put(tenantName, plan);
- }
-
- @Override
- public void deactivateDefaultPaymentInstrument(TenantName tenant) {
- paymentInstruments.removeIf(instrumentOwner -> instrumentOwner.getTenantName().equals(tenant));
- }
-
- @Override
- public Optional<CollectionMethod> getCollectionMethod(TenantName tenantName) {
- return Optional.ofNullable(collectionMethods.get(tenantName));
- }
-
- @Override
- public void setCollectionMethod(TenantName tenantName, CollectionMethod collectionMethod) {
- collectionMethods.put(tenantName, collectionMethod);
- }
-
- @Override
- public List<Bill> readBillsForTenant(TenantName tenant) {
- return invoices.entrySet().stream()
- .filter(entry -> entry.getValue().equals(tenant))
- .map(Map.Entry::getKey)
- .map(invoiceId -> {
- var items = lineItems.getOrDefault(invoiceId, List.of());
- var status = statuses.get(invoiceId);
- var start = startTimes.get(invoiceId);
- var end = endTimes.get(invoiceId);
- return new Bill(invoiceId, tenant, status, items, start, end, exportedInvoiceId(invoiceId));
- })
- .toList();
- }
-
- @Override
- public List<Bill> readBills() {
- return invoices.keySet().stream()
- .map(invoiceId -> {
- var tenant = invoices.get(invoiceId);
- var items = lineItems.getOrDefault(invoiceId, List.of());
- var status = statuses.get(invoiceId);
- var start = startTimes.get(invoiceId);
- var end = endTimes.get(invoiceId);
- return new Bill(invoiceId, tenant, status, items, start, end, exportedInvoiceId(invoiceId));
- })
- .toList();
- }
-
- @Override
- public void maintain() {}
-
- @Override
- public void setExportedInvoiceId(Bill.Id billId, String invoiceId) {
- exportedInvoiceIds.put(billId, invoiceId);
- }
-
- @Override
- public void setExportedInvoiceItemId(String lineItemId, String invoiceItemId) { }
-
- private String exportedInvoiceId(Bill.Id billId) {
- return exportedInvoiceIds.getOrDefault(billId, null);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java
deleted file mode 100644
index 4aab4a47fc6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporter.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.vespa.hosted.controller.tenant.BillingReference;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-
-public interface BillingReporter {
- BillingReference maintainTenant(CloudTenant tenant);
-
- InvoiceUpdate maintainInvoice(CloudTenant teannt, Bill bill);
-
- /** Export a bill to a payment service. Returns the invoice ID in the external system. */
- default String exportBill(Bill bill, String exportMethod, CloudTenant tenant) {
- return "NOT_IMPLEMENTED";
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java
deleted file mode 100644
index 21efa954cb0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillingReporterMock.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.vespa.hosted.controller.tenant.BillingReference;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-
-import java.math.BigDecimal;
-import java.time.Clock;
-import java.time.ZonedDateTime;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-
-public class BillingReporterMock implements BillingReporter {
- private final Clock clock;
- private final BillingDatabaseClient dbClient;
-
- private final Map<Bill.Id, InvoiceUpdate> exportedBills = new HashMap<>();
-
- public BillingReporterMock(Clock clock, BillingDatabaseClient dbClient) {
- this.clock = clock;
- this.dbClient = dbClient;
- }
-
- @Override
- public BillingReference maintainTenant(CloudTenant tenant) {
- return new BillingReference(UUID.randomUUID().toString(), clock.instant());
- }
-
- @Override
- public InvoiceUpdate maintainInvoice(CloudTenant tenant, Bill bill) {
- if (! exportedBills.containsKey(bill.id())) {
- // Given that it has been exported earlier (caller's responsibility), we can assume it has been removed.
- return InvoiceUpdate.removed(bill.id());
- }
- if (exportedBills.get(bill.id()).type() == InvoiceUpdate.Type.MODIFIED) {
- // modifyInvoice() has been called -> add a marker line item
- if (bill.status() != BillStatus.OPEN) throw new IllegalArgumentException("Bill should be OPEN");
- dbClient.addLineItem(bill.tenant(), maintainedMarkerItem(), Optional.of(bill.id()));
- }
- return exportedBills.get(bill.id());
- }
-
- @Override
- public String exportBill(Bill bill, String exportMethod, CloudTenant tenant) {
- // Replace bill with a copy with exportedId set
- var exportedId = "EXPORTED-" + bill.id().value();
- exportedBills.put(bill.id(), InvoiceUpdate.modifiable(bill.id(), null));
- dbClient.setExportedInvoiceId(bill.id(), exportedId);
- return exportedId;
- }
-
- public void modifyInvoice(Bill.Id billId) {
- ensureExported(billId);
- var itemsUpdate = new InvoiceUpdate.ItemsUpdate(1, 0, 0);
- exportedBills.put(billId, InvoiceUpdate.modifiable(billId, itemsUpdate));
- }
-
- public void freezeInvoice(Bill.Id billId) {
- ensureExported(billId);
- exportedBills.put(billId, InvoiceUpdate.unmodifiable(billId));
- }
-
- public void payInvoice(Bill.Id billId) {
- ensureExported(billId);
- exportedBills.put(billId, InvoiceUpdate.paid(billId));
- }
-
- public void voidInvoice(Bill.Id billId) {
- ensureExported(billId);
- exportedBills.put(billId, InvoiceUpdate.voided(billId));
- }
-
- // Emulates deleting a bill in the external system.
- public void deleteInvoice(Bill.Id billId) {
- ensureExported(billId);
- exportedBills.remove(billId);
- }
-
- private void ensureExported(Bill.Id billId) {
- if (! exportedBills.containsKey(billId)) throw new IllegalArgumentException("Bill not exported");
- }
-
- private static Bill.LineItem maintainedMarkerItem() {
- return new Bill.LineItem("maintained", "", BigDecimal.valueOf(0.0), "", "", ZonedDateTime.now());
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionMethod.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionMethod.java
deleted file mode 100644
index 87c9a13d64a..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionMethod.java
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-public enum CollectionMethod {
- NONE,
- EPAY,
- INVOICE,
- AUTO // Deprecated - this has never been serialized and can be removed in subsequent release
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionResult.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionResult.java
deleted file mode 100644
index 4e83d90f6b0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CollectionResult.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Object to carry the result of {@link BillingController#setCollectionMethod}
- *
- * @author smorgrav
- */
-public class CollectionResult {
-
- private final Optional<String> errorMessage;
-
- private CollectionResult(Optional<String> errorMessage) {
- this.errorMessage = errorMessage;
- }
-
- public static CollectionResult success() {
- return new CollectionResult(Optional.empty());
- }
-
- public static CollectionResult error(String errorMessage) {
- return new CollectionResult(Optional.of(errorMessage));
- }
-
- public boolean isSuccess() {
- return errorMessage.isEmpty();
- }
-
- public Optional<String> getErrorMessage() {
- return errorMessage;
- }
-
- @Override
- public String toString() {
- return "CollectionResult{" +
- "errorMessage=" + errorMessage +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- CollectionResult that = (CollectionResult) o;
- return Objects.equals(errorMessage, that.errorMessage);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(errorMessage);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java
deleted file mode 100644
index ddcd5308986..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/CostCalculator.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.CostInfo;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceUsage;
-
-import java.math.BigDecimal;
-
-/**
- * @author ogronnesby
- */
-public interface CostCalculator {
-
- /** Calculate the cost for the given usage */
- CostInfo calculate(ResourceUsage usage);
-
- /** Estimate the cost for the given resources */
- double calculate(NodeResources resources);
-
- /** CPU unit price */
- BigDecimal getCpuPrice();
-
- /** Memory unit price */
- BigDecimal getMemoryPrice();
-
- /** Disk unit price */
- BigDecimal getDiskPrice();
-
- /** GPU unit price */
- BigDecimal getGpuPrice();
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java
deleted file mode 100644
index bc9f82c212b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InstrumentOwner.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.config.provision.TenantName;
-
-import java.util.Objects;
-
-/**
- * @author olaa
- */
-public class InstrumentOwner {
-
- private final TenantName tenantName;
- private final String userId;
- private final String paymentInstrumentId;
- private final boolean isDefault;
-
- public InstrumentOwner(TenantName tenantName, String userId, String paymentInstrumentId, boolean isDefault) {
- this.tenantName = tenantName;
- this.userId = userId;
- this.paymentInstrumentId = paymentInstrumentId;
- this.isDefault = isDefault;
- }
-
- public TenantName getTenantName() {
- return tenantName;
- }
-
- public String getUserId() {
- return userId;
- }
-
- public String getPaymentInstrumentId() {
- return paymentInstrumentId;
- }
-
- public boolean isDefault() {
- return isDefault;
- }
-
- @Override
- public String toString() {
- return String.format(
- "Tenant: %s\nCusomer ID: %s\nPayment Instrument ID: %s\nIs default: %s",
- tenantName.value(),
- userId,
- paymentInstrumentId,
- isDefault
- );
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- InstrumentOwner other = (InstrumentOwner) o;
- return this.tenantName.equals(other.getTenantName()) &&
- this.userId.equals(other.getUserId()) &&
- this.paymentInstrumentId.equals(other.getPaymentInstrumentId()) &&
- this.isDefault() == other.isDefault();
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(tenantName, userId, paymentInstrumentId, isDefault);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java
deleted file mode 100644
index 4dfd9ef1fee..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/InvoiceUpdate.java
+++ /dev/null
@@ -1,85 +0,0 @@
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import java.util.Optional;
-
-/**
- * Helper to track changes to an invoice made by the controller. This should be independent
- * of which external system that is being used.
- *
- * @author gjoranv
- */
-public record InvoiceUpdate(Bill.Id billId, Type type, Optional<ItemsUpdate> itemsUpdate) {
-
- public enum Type {
- UNMODIFIED, // The invoice was modifiable, but not modified by us
- MODIFIED, // The invoice was modified by us
- UNMODIFIABLE, // The invoice was unmodifiable in the external system
- REMOVED, // Removed from the external system, presumably for a valid reason
- PAID, // Reported paid from the external system
- VOIDED // Voided in the external system
- }
-
- public InvoiceUpdate {
- if (type != Type.MODIFIED && itemsUpdate.isPresent())
- throw new IllegalArgumentException("Items update is only allowed for modified invoices. Update type was " + type);
- }
-
- public static InvoiceUpdate modifiable(Bill.Id billId, ItemsUpdate itemsUpdate) {
- if (itemsUpdate == null || itemsUpdate.isEmpty()) {
- return new InvoiceUpdate(billId, Type.UNMODIFIED, Optional.empty());
- } else {
- return new InvoiceUpdate(billId, Type.MODIFIED, Optional.of(itemsUpdate));
- }
- }
-
- public static InvoiceUpdate unmodifiable(Bill.Id billId) {
- return new InvoiceUpdate(billId, Type.UNMODIFIABLE, Optional.empty());
- }
-
- public static InvoiceUpdate removed(Bill.Id billId) {
- return new InvoiceUpdate(billId, Type.REMOVED, Optional.empty());
- }
-
- public static InvoiceUpdate paid(Bill.Id billId) {
- return new InvoiceUpdate(billId, Type.PAID, Optional.empty());
- }
-
- public static InvoiceUpdate voided(Bill.Id billId) {
- return new InvoiceUpdate(billId, Type.VOIDED, Optional.empty());
- }
-
-
- public record ItemsUpdate(int itemsAdded, int itemsRemoved, int itemsModified) {
-
- public boolean isEmpty() {
- return itemsAdded == 0 && itemsRemoved == 0 && itemsModified == 0;
- }
-
- public static ItemsUpdate empty() {
- return new ItemsUpdate(0, 0, 0);
- }
-
- public static class Counter {
- private int itemsAdded = 0;
- private int itemsRemoved = 0;
- private int itemsModified = 0;
-
- public void addedItem() {
- itemsAdded++;
- }
-
- public void removedItem() {
- itemsRemoved++;
- }
-
- public void modifiedItem() {
- itemsModified++;
- }
-
- public ItemsUpdate finish() {
- return new ItemsUpdate(itemsAdded, itemsRemoved, itemsModified);
- }
- }
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
deleted file mode 100644
index b50018c187c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/MockBillingController.java
+++ /dev/null
@@ -1,212 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.tenant.TaxId;
-
-import java.math.BigDecimal;
-import java.time.Clock;
-import java.time.LocalDate;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Stream;
-
-/**
- * @author olaa
- */
-public class MockBillingController implements BillingController {
-
- private final Clock clock;
- private final BillingDatabaseClient dbClient;
-
- PlanId defaultPlan = PlanId.from("trial");
- List<TenantName> tenants = new ArrayList<>();
- Map<TenantName, PlanId> plans = new HashMap<>();
- Map<TenantName, List<Bill>> committedBills = new HashMap<>();
- public Map<TenantName, Bill> uncommittedBills = new HashMap<>();
- Map<TenantName, List<Bill.LineItem>> unusedLineItems = new HashMap<>();
- Map<TenantName, CollectionMethod> collectionMethod = new HashMap<>();
-
- public MockBillingController(Clock clock, BillingDatabaseClient dbClient) {
- this.clock = clock;
- this.dbClient = dbClient;
- }
-
- @Override
- public PlanId getPlan(TenantName tenant) {
- return plans.getOrDefault(tenant, PlanId.from("trial"));
- }
-
- @Override
- public List<TenantName> tenantsWithPlan(List<TenantName> tenants, PlanId planId) {
- return tenants.stream()
- .filter(t -> plans.getOrDefault(t, PlanId.from("trial")).equals(planId))
- .toList();
- }
-
- @Override
- public Quota getQuota(TenantName tenant) {
- return Quota.unlimited().withMaxClusterSize(5);
- }
-
- @Override
- public PlanResult setPlan(TenantName tenant, PlanId planId, boolean hasDeployments, boolean isAccountant) {
- plans.put(tenant, planId);
- return PlanResult.success();
- }
-
- @Override
- public Bill.Id createBillForPeriod(TenantName tenant, ZonedDateTime startTime, ZonedDateTime endTime, String agent) {
- var billId = Bill.Id.of("id-123");
- committedBills.computeIfAbsent(tenant, l -> new ArrayList<>())
- .add(new Bill(
- billId,
- tenant,
- StatusHistory.open(clock),
- List.of(),
- startTime,
- endTime
- ));
- return billId;
- }
-
- @Override
- public Bill createUncommittedBill(TenantName tenant, LocalDate until) {
- return uncommittedBills.getOrDefault(tenant, emptyBill());
- }
-
- @Override
- public Map<TenantName, Bill> createUncommittedBills(LocalDate until) {
- return uncommittedBills;
- }
-
- @Override
- public List<Bill.LineItem> getUnusedLineItems(TenantName tenant) {
- return unusedLineItems.getOrDefault(tenant, List.of());
- }
-
- @Override
- public void addLineItem(TenantName tenant, String description, BigDecimal amount, Optional<Bill.Id> billId, String agent) {
- if (billId.isPresent()) {
- throw new UnsupportedOperationException();
- } else {
- unusedLineItems.computeIfAbsent(tenant, l -> new ArrayList<>())
- .add(new Bill.LineItem(
- "line-item-id",
- description,
- amount,
- "paid",
- agent,
- ZonedDateTime.now()));
- }
- }
-
- @Override
- public void deleteLineItem(String lineItemId) {
- unusedLineItems.values()
- .forEach(lineItems -> lineItems.
- removeIf(lineItem -> lineItem.id().equals(lineItemId))
- );
- }
-
- @Override
- public List<Bill> getBillsForTenant(TenantName tenant) {
- return committedBills.getOrDefault(tenant, List.of());
- }
-
- @Override
- public Bill getBill(Bill.Id billId) {
- return committedBills.values().stream()
- .flatMap(Collection::stream)
- .filter(bill -> bill.id().equals(billId))
- .findFirst()
- .orElseThrow();
- }
-
- @Override
- public CollectionMethod getCollectionMethod(TenantName tenant) {
- return collectionMethod.getOrDefault(tenant, CollectionMethod.AUTO);
- }
-
- @Override
- public CollectionResult setCollectionMethod(TenantName tenant, CollectionMethod method) {
- collectionMethod.put(tenant, method);
- return CollectionResult.success();
- }
-
- @Override
- public boolean tenantsWithPlanUnderLimit(Plan plan, int limit) {
- if (limit < 0) return true;
-
- var count = Stream.concat(tenants.stream(), plans.keySet().stream())
- .distinct()
- .map(tenant -> plans.getOrDefault(tenant, defaultPlan))
- .filter(p -> p.equals(plan.id()))
- .count();
-
- return count < limit;
- }
-
- @Override
- public AcceptedCountries getAcceptedCountries() {
- return new AcceptedCountries(List.of(
- new AcceptedCountries.Country(
- "NO", "Norway", true,
- List.of(new AcceptedCountries.TaxType("no_vat", "Norwegian VAT number", "[0-9]{9}MVA", "123456789MVA"))),
- new AcceptedCountries.Country(
- "CA", "Canada", true,
- List.of(new AcceptedCountries.TaxType("ca_gst_hst", "Canadian GST/HST number", "([0-9]{9}) ?RT ?([0-9]{4})", "123456789RT0002"),
- new AcceptedCountries.TaxType("ca_pst_bc", "Canadian PST number (British Columbia)", "PST-?([0-9]{4})-?([0-9]{4})", "PST-1234-5678")))
- ));
- }
-
- @Override
- public void validateTaxId(TaxId id) throws IllegalArgumentException {
- if (id.isEmpty() || id.isLegacy() || id.country().isEmpty()) return;
- if (!List.of("eu_vat", "no_vat").contains(id.type().value()))
- throw new IllegalArgumentException("Unknown tax id type '%s'".formatted(id.type().value()));
- if (!id.code().value().matches("\\w+"))
- throw new IllegalArgumentException("Invalid tax id code '%s'".formatted(id.code().value()));
- }
-
-
- public void setTenants(List<TenantName> tenants) {
- this.tenants = tenants;
- }
-
- private PaymentInstrument createInstrument(String id) {
- return new PaymentInstrument(id,
- "name",
- "displayText",
- "brand",
- "type",
- "endingWith",
- "expiryDate",
- "addressLine1",
- "addressLine2",
- "zip",
- "city",
- "state",
- "country");
- }
-
- public void addBill(TenantName tenantName, Bill bill, boolean committed) {
- if (committed)
- committedBills.computeIfAbsent(tenantName, i -> new ArrayList<>())
- .add(bill);
- else
- uncommittedBills.put(tenantName, bill);
- }
-
- private Bill emptyBill() {
- var start = clock.instant().atZone(ZoneOffset.UTC);
- var end = clock.instant().atZone(ZoneOffset.UTC);
- return new Bill(Bill.Id.of("empty"), TenantName.defaultName(), StatusHistory.open(clock), List.of(), start, end);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java
deleted file mode 100644
index 63771c3366a..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PaymentInstrument.java
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-/**
- * @author olaa
- */
-public class PaymentInstrument {
-
- private final String id;
- private final String nameOnCard;
- private final String displayText;
- private final String brand;
- private final String type;
- private final String endingWith;
- private final String expiryDate;
- private final String addressLine1;
- private final String addressLine2;
- private final String city;
- private final String state;
- private final String zip;
- private final String country;
-
- public PaymentInstrument(String id, String nameOnCard, String displayText, String brand, String type, String endingWith, String expiryDate, String addressLine1, String addressLine2, String zip, String city, String state, String country) {
- this.id = id;
- this.nameOnCard = nameOnCard;
- this.displayText = displayText;
- this.brand = brand;
- this.type = type;
- this.endingWith = endingWith;
- this.expiryDate = expiryDate;
- this.addressLine1 = addressLine1;
- this.addressLine2 = addressLine2;
- this.zip = zip;
- this.city = city;
- this.state = state;
- this.country = country;
- }
-
- public String getId() {
- return id;
- }
-
- public String getNameOnCard() {
- return nameOnCard;
- }
-
- public String getDisplayText() {
- return displayText;
- }
-
- public String getBrand() {
- return brand;
- }
-
- public String getType() {
- return type;
- }
-
- public String getEndingWith() {
- return endingWith;
- }
-
- public String getExpiryDate() {
- return expiryDate;
- }
-
- public String getAddressLine1() {
- return addressLine1;
- }
-
- public String getAddressLine2() {
- return addressLine2;
- }
-
- public String getCity() {
- return city;
- }
-
- public String getState() {
- return state;
- }
-
- public String getZip() {
- return zip;
- }
-
- public String getCountry() {
- return country;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java
deleted file mode 100644
index 5e964603db1..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Plan.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-/**
- * A Plan knows about the billing calculations and the default quota for any tenant associated with the Plan.
- * The Plan can also enforce transitions from one plan to another.
- *
- * @author ogronnesby
- */
-public interface Plan {
-
- /** Unique ID for the plan */
- PlanId id();
-
- /** A string to be used for display purposes */
- String displayName();
-
- /** The cost calculator for this plan */
- CostCalculator calculator();
-
- /** The quota for this plan */
- QuotaCalculator quota();
-
- /** Is this a plan that is billed */
- boolean isBilled();
-
- /** Is this a plan that gets on-call support */
- boolean isSupported();
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java
deleted file mode 100644
index 061f8731b41..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanId.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import java.util.Objects;
-
-/**
- * @author olaa
- */
-public class PlanId {
-
- private final String value;
-
- public PlanId(String value) {
- if (value.isBlank())
- throw new IllegalArgumentException("Id must be non-blank.");
- this.value = value;
- }
-
- public static PlanId from(String value) {
- return new PlanId(value);
- }
-
- public String value() { return value; }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- PlanId id = (PlanId) o;
- return Objects.equals(value, id.value);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(value);
- }
-
- @Override
- public String toString() {
- return "plan '" + value + "'";
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java
deleted file mode 100644
index c0bd0dd29cd..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistry.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Registry of all current plans we have support for
- *
- * @author ogronnesby
- */
-public interface PlanRegistry {
-
- /** Get the default plan */
- Plan defaultPlan();
-
- /** Get a plan given a plan ID */
- Optional<Plan> plan(PlanId planId);
-
- /** Get a set of all plans */
- List<Plan> all();
-
- default Plan require(String planId) {
- return plan(planId).orElseThrow();
- }
-
- /** Get a plan give a plan ID */
- default Optional<Plan> plan(String planId) {
- if (planId == null || planId.isBlank())
- return Optional.empty();
- return plan(PlanId.from(planId));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java
deleted file mode 100644
index 5af4d0cff29..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanRegistryMock.java
+++ /dev/null
@@ -1,168 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.CostInfo;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceUsage;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Stream;
-
-public class PlanRegistryMock implements PlanRegistry {
-
- public static final Plan freeTrial = new MockPlan("trial", false, false, 0, 0, 0, 0, 200, "Free Trial - for testing purposes");
- public static final Plan paidPlan = new MockPlan("paid", true, true, "0.09", "0.009", "0.0003", "0.075", 500, "Paid Plan - for testing purposes");
- public static final Plan nonePlan = new MockPlan("none", false, false, 0, 0, 0, 0, 0, "None Plan - for testing purposes");
-
- @Override
- public Plan defaultPlan() {
- return freeTrial;
- }
-
- @Override
- public Optional<Plan> plan(PlanId planId) {
- return Stream.of(freeTrial, paidPlan, nonePlan)
- .filter(p -> p.id().equals(planId))
- .findAny();
- }
-
- @Override
- public List<Plan> all() {
- return List.of(freeTrial, paidPlan, nonePlan);
- }
-
- private static class MockPlan implements Plan {
- private final PlanId planId;
- private final String description;
- private final CostCalculator costCalculator;
- private final QuotaCalculator quotaCalculator;
- private final boolean billed;
- private final boolean supported;
-
- public MockPlan(String planId, boolean billed, boolean supported, double cpuPrice, double memPrice, double dgbPrice, double gpuPrice, int quota, String description) {
- this(PlanId.from(planId), billed, supported, new MockCostCalculator(cpuPrice, memPrice, dgbPrice, gpuPrice), () -> createQuota(quota), description);
- }
-
- public MockPlan(String planId, boolean billed, boolean supported, String cpuPrice, String memPrice, String dgbPrice, String gpuPrice, int quota, String description) {
- this(PlanId.from(planId), billed, supported, new MockCostCalculator(cpuPrice, memPrice, dgbPrice, gpuPrice), () -> createQuota(quota), description);
- }
-
- private static Quota createQuota(int quota) {
- return quota == 0 ? Quota.zero() : Quota.unlimited().withBudget(quota);
- }
-
- public MockPlan(PlanId planId, boolean billed, boolean supported, MockCostCalculator calculator, QuotaCalculator quota, String description) {
- this.planId = planId;
- this.billed = billed;
- this.supported = supported;
- this.costCalculator = calculator;
- this.quotaCalculator = quota;
- this.description = description;
- }
-
- @Override
- public PlanId id() {
- return planId;
- }
-
- @Override
- public String displayName() {
- return description;
- }
-
- @Override
- public CostCalculator calculator() {
- return costCalculator;
- }
-
- @Override
- public QuotaCalculator quota() {
- return quotaCalculator;
- }
-
- @Override
- public boolean isBilled() {
- return billed;
- }
-
- @Override
- public boolean isSupported() {
- return supported;
- }
- }
-
- private static class MockCostCalculator implements CostCalculator {
- private static final BigDecimal millisPerHour = BigDecimal.valueOf(60 * 60 * 1000);
- private final BigDecimal cpuHourCost;
- private final BigDecimal memHourCost;
- private final BigDecimal dgbHourCost;
- private final BigDecimal gpuHourCost;
-
- public MockCostCalculator(String cpuPrice, String memPrice, String dgbPrice, String gpuPrice) {
- this(new BigDecimal(cpuPrice), new BigDecimal(memPrice), new BigDecimal(dgbPrice), new BigDecimal(gpuPrice));
- }
-
- public MockCostCalculator(double cpuPrice, double memPrice, double dgbPrice, double gpuPrice) {
- this(BigDecimal.valueOf(cpuPrice), BigDecimal.valueOf(memPrice), BigDecimal.valueOf(dgbPrice), BigDecimal.valueOf(gpuPrice));
- }
-
- public MockCostCalculator(BigDecimal cpuPrice, BigDecimal memPrice, BigDecimal dgbPrice, BigDecimal gpuPrice) {
- this.cpuHourCost = cpuPrice;
- this.memHourCost = memPrice;
- this.dgbHourCost = dgbPrice;
- this.gpuHourCost = gpuPrice;
- }
-
- @Override
- public CostInfo calculate(ResourceUsage usage) {
- var cpuCost = usage.getCpuMillis().multiply(cpuHourCost).divide(millisPerHour, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP);
- var memCost = usage.getMemoryMillis().multiply(memHourCost).divide(millisPerHour, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP);
- var dgbCost = usage.getDiskMillis().multiply(dgbHourCost).divide(millisPerHour, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP);
- var gpuCost = usage.getGpuMillis().multiply(gpuHourCost).divide(millisPerHour, RoundingMode.HALF_UP).setScale(2, RoundingMode.HALF_UP);
-
- return new CostInfo(
- usage.getApplicationId(),
- usage.getZoneId(),
- usage.getCpuMillis().divide(millisPerHour, RoundingMode.HALF_UP),
- usage.getMemoryMillis().divide(millisPerHour, RoundingMode.HALF_UP),
- usage.getDiskMillis().divide(millisPerHour, RoundingMode.HALF_UP),
- usage.getGpuMillis().divide(millisPerHour, RoundingMode.HALF_UP),
- cpuCost,
- memCost,
- dgbCost,
- gpuCost,
- usage.getArchitecture(),
- usage.getMajorVersion(),
- usage.getCloudAccount()
- );
- }
-
- @Override
- public double calculate(NodeResources resources) {
- return resources.cost();
- }
-
- @Override
- public BigDecimal getCpuPrice() {
- return cpuHourCost;
- }
-
- @Override
- public BigDecimal getMemoryPrice() {
- return memHourCost;
- }
-
- @Override
- public BigDecimal getDiskPrice() {
- return dgbHourCost;
- }
-
- @Override
- public BigDecimal getGpuPrice() {
- return gpuHourCost;
- }
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanResult.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanResult.java
deleted file mode 100644
index 35e7fcbcf6e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/PlanResult.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Result of {@link BillingController#setPlan}
- *
- * @author olaa
- */
-public class PlanResult {
-
- private final Optional<String> errorMessage;
-
- private PlanResult(Optional<String> errorMessage) {
- this.errorMessage = errorMessage;
- }
-
- public static PlanResult success() {
- return new PlanResult(Optional.empty());
- }
-
- public static PlanResult error(String errorMessage) {
- return new PlanResult(Optional.of(errorMessage));
- }
-
- public boolean isSuccess() {
- return errorMessage.isEmpty();
- }
-
- public Optional<String> getErrorMessage() {
- return errorMessage;
- }
-
- @Override
- public String toString() {
- return "PlanResult{" +
- "errorMessage=" + errorMessage +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- PlanResult that = (PlanResult) o;
- return Objects.equals(errorMessage, that.errorMessage);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(errorMessage);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java
deleted file mode 100644
index ca339f9cf15..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/Quota.java
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import java.math.BigDecimal;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalInt;
-
-/**
- * Quota information transmitted to the configserver on deploy. All limits are represented
- * with an Optional type where the empty optional means unlimited resource use.
- *
- * @author andreer
- * @author ogronnesby
- */
-public class Quota {
- private static final Quota UNLIMITED = new Quota(OptionalInt.empty(), Optional.empty());
- private static final Quota ZERO = new Quota(OptionalInt.of(0), Optional.of(BigDecimal.ZERO));
-
- private final OptionalInt maxClusterSize;
- private final Optional<BigDecimal> budget; // in USD/hr, as calculated by NodeResources
-
- private Quota(OptionalInt maxClusterSize, Optional<BigDecimal> budget) {
- this.maxClusterSize = Objects.requireNonNull(maxClusterSize);
- this.budget = Objects.requireNonNull(budget);
- }
-
- public Quota withMaxClusterSize(int clusterSize) {
- return new Quota(OptionalInt.of(clusterSize), budget);
- }
-
- /** Construct a Quota that allows zero resource usage */
- public static Quota zero() {
- return ZERO;
- }
-
- /** Construct a Quota that allows unlimited resource usage */
- public static Quota unlimited() {
- return UNLIMITED;
- }
-
- public boolean isUnlimited() {
- return budget.isEmpty() && maxClusterSize().isEmpty();
- }
-
- public Quota withBudget(BigDecimal budget) {
- return new Quota(maxClusterSize, Optional.ofNullable(budget));
- }
-
- public Quota withBudget(int budget) {
- return withBudget(BigDecimal.valueOf(budget));
- }
-
- public Quota withoutBudget() {
- return new Quota(maxClusterSize, Optional.empty());
- }
-
- /** Maximum number of nodes in a cluster in a Vespa deployment */
- public OptionalInt maxClusterSize() {
- return maxClusterSize;
- }
-
- /** Maximum $/hour run-rate for the Vespa deployment */
- public Optional<BigDecimal> budget() {
- return budget;
- }
-
- public Quota subtractUsage(double rate) {
- if (budget().isEmpty()) return this; // (unlimited - rate) is still unlimited
- return this.withBudget(budget().get().subtract(BigDecimal.valueOf(rate)));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Quota quota = (Quota) o;
- return maxClusterSize.equals(quota.maxClusterSize) &&
- this.budget.map(BigDecimal::stripTrailingZeros).equals(
- quota.budget.map(BigDecimal::stripTrailingZeros));
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(maxClusterSize, budget);
- }
-
- @Override
- public String toString() {
- return "Quota{" +
- "maxClusterSize=" + maxClusterSize +
- ", budget=" + budget +
- '}';
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/QuotaCalculator.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/QuotaCalculator.java
deleted file mode 100644
index 9511d1109ff..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/QuotaCalculator.java
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-/**
- * Calculates the quota. This is used in the context of a {@link Plan}.
- *
- * @author ogronnesby
- */
-public interface QuotaCalculator {
- /** Calculate the quota for a given environment */
- Quota calculate();
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistory.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistory.java
deleted file mode 100644
index 788995555a8..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistory.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import java.time.Clock;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-/**
- * @author ogronnesby
- */
-public class StatusHistory {
- SortedMap<ZonedDateTime, BillStatus> history;
-
- public StatusHistory(SortedMap<ZonedDateTime, BillStatus> history) {
- // Validate the given history
- var iter = history.values().iterator();
- BillStatus next = iter.hasNext() ? iter.next() : null;
- while (iter.hasNext()) {
- var current = next;
- next = iter.next();
- if (! validateStatus(current, next)) {
- throw new IllegalArgumentException("Invalid transition from " + current + " to " + next);
- }
- }
-
- this.history = history;
- }
-
- public static StatusHistory open(Clock clock) {
- var now = clock.instant().atZone(ZoneOffset.UTC);
- return new StatusHistory(
- new TreeMap<>(Map.of(now, BillStatus.OPEN))
- );
- }
-
- public BillStatus current() {
- return history.get(history.lastKey());
- }
-
- public SortedMap<ZonedDateTime, BillStatus> getHistory() {
- return history;
- }
-
- public void checkValidTransition(BillStatus newStatus) {
- if (! validateStatus(current(), newStatus)) {
- throw new IllegalArgumentException("Invalid transition from " + current() + " to " + newStatus);
- }
- }
-
- private static boolean validateStatus(BillStatus current, BillStatus newStatus) {
- return switch(current) {
- case OPEN -> true;
- case FROZEN -> newStatus != BillStatus.OPEN; // This could be subject to change.
- case SUCCESSFUL -> newStatus == BillStatus.SUCCESSFUL;
- case VOID -> newStatus == BillStatus.VOID;
- };
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/package-info.java
deleted file mode 100644
index d05e9aa4c09..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/billing/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificate.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificate.java
deleted file mode 100644
index 09120f8cd21..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificate.java
+++ /dev/null
@@ -1,161 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-import java.util.List;
-import java.util.Optional;
-
-/**
- * This holds information about an application's endpoint certificate.
- *
- * @author andreer
- */
-public record EndpointCertificate(String keyName, String certName, int version, long lastRequested,
- String rootRequestId, // The id of the first request made for this certificate. Should not change.
- Optional<String> leafRequestId, // The id of the last known request made for this certificate. Changes on refresh, may be outdated!
- List<String> requestedDnsSans, String issuer, Optional<Long> expiry,
- Optional<Long> lastRefreshed, Optional<String> generatedId) {
-
- public EndpointCertificate withGeneratedId(String generatedId) {
- return new EndpointCertificate(
- this.keyName,
- this.certName,
- this.version,
- this.lastRequested,
- this.rootRequestId,
- this.leafRequestId,
- this.requestedDnsSans,
- this.issuer,
- this.expiry,
- this.lastRefreshed,
- Optional.of(generatedId));
- }
-
- public EndpointCertificate withKeyName(String keyName) {
- return new EndpointCertificate(
- keyName,
- this.certName,
- this.version,
- this.lastRequested,
- this.rootRequestId,
- this.leafRequestId,
- this.requestedDnsSans,
- this.issuer,
- this.expiry,
- this.lastRefreshed,
- this.generatedId);
- }
-
- public EndpointCertificate withCertName(String certName) {
- return new EndpointCertificate(
- this.keyName,
- certName,
- this.version,
- this.lastRequested,
- this.rootRequestId,
- this.leafRequestId,
- this.requestedDnsSans,
- this.issuer,
- this.expiry,
- this.lastRefreshed,
- this.generatedId);
- }
-
- public EndpointCertificate withVersion(int version) {
- return new EndpointCertificate(
- this.keyName,
- this.certName,
- version,
- this.lastRequested,
- this.rootRequestId,
- this.leafRequestId,
- this.requestedDnsSans,
- this.issuer,
- this.expiry,
- this.lastRefreshed,
- this.generatedId);
- }
-
- public EndpointCertificate withLastRequested(long lastRequested) {
- return new EndpointCertificate(
- this.keyName,
- this.certName,
- this.version,
- lastRequested,
- this.rootRequestId,
- this.leafRequestId,
- this.requestedDnsSans,
- this.issuer,
- this.expiry,
- this.lastRefreshed,
- this.generatedId);
- }
-
- public EndpointCertificate withLastRefreshed(long lastRefreshed) {
- return new EndpointCertificate(
- this.keyName,
- this.certName,
- this.version,
- this.lastRequested,
- this.rootRequestId,
- this.leafRequestId,
- this.requestedDnsSans,
- this.issuer,
- this.expiry,
- Optional.of(lastRefreshed),
- this.generatedId);
- }
-
- public EndpointCertificate withRootRequestId(String rootRequestId) {
- return new EndpointCertificate(
- this.keyName,
- this.certName,
- this.version,
- this.lastRequested,
- rootRequestId,
- this.leafRequestId,
- this.requestedDnsSans,
- this.issuer,
- this.expiry,
- this.lastRefreshed,
- this.generatedId);
- }
-
- public EndpointCertificate withLeafRequestId(Optional<String> leafRequestId) {
- return new EndpointCertificate(
- this.keyName,
- this.certName,
- this.version,
- this.lastRequested,
- this.rootRequestId,
- leafRequestId,
- this.requestedDnsSans,
- this.issuer,
- this.expiry,
- this.lastRefreshed,
- this.generatedId);
- }
-
- /** Returns whether given DNS name matches any of the requested SANs in this */
- public boolean sanMatches(String dnsName) {
- return sanMatches(dnsName, requestedDnsSans);
- }
-
- static boolean sanMatches(String dnsName, List<String> sanDnsNames) {
- return sanDnsNames.stream().anyMatch(sanDnsName -> sanMatches(dnsName, sanDnsName));
- }
-
- private static boolean sanMatches(String dnsName, String sanDnsName) {
- String[] sanNameParts = sanDnsName.split("\\.");
- String[] dnsNameParts = dnsName.split("\\.");
- if (sanNameParts.length != dnsNameParts.length || sanNameParts.length == 0) {
- return false;
- }
- for (int i = 0; i < sanNameParts.length; i++) {
- if (!sanNameParts[i].equals("*") && !sanNameParts[i].equals(dnsNameParts[i])) {
- return false;
- }
- }
- return true;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateDetails.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateDetails.java
deleted file mode 100644
index ad4b360aae2..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateDetails.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-import java.util.List;
-
-/**
- * This record is used when requesting additional details about an application's endpoint certificate from the provider.
- *
- * @author andreer
- */
-public record EndpointCertificateDetails(
- String requestId,
- String requestor,
- String status,
- String ticketId,
- String athenzDomain,
- List<EndpointCertificateRequest.DnsNameStatus> dnsNames,
- String durationSec,
- String expiry,
- String privateKeyKgname,
- String privateKeyKeyname,
- String privateKeyVersion,
- String certKeyKgname,
- String certKeyKeyname,
- String certKeyVersion,
- String createTime,
- boolean expiryProtection,
- String publicKeyAlgo,
- String issuer,
- String serial
-) { }
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateException.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateException.java
deleted file mode 100644
index 8ee1f313e6d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateException.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-/**
- * @author andreer
- */
-public class EndpointCertificateException extends RuntimeException {
-
- private final Type type;
-
- public EndpointCertificateException(Type type, String message) {
- super(message);
- this.type = type;
- }
-
- public EndpointCertificateException(Type type, String message, Throwable cause) {
- super(message, cause);
- this.type = type;
- }
-
- public Type type() {
- return type;
- }
-
- public enum Type {
- CERT_NOT_AVAILABLE,
- VERIFICATION_FAILURE
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateProvider.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateProvider.java
deleted file mode 100644
index 30e9295f347..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateProvider.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Generates an endpoint certificate for an application instance.
- *
- * @author andreer
- */
-public interface EndpointCertificateProvider {
-
- EndpointCertificate requestCaSignedCertificate(String endpointCertificatePrefix, List<String> dnsNames, Optional<EndpointCertificate> currentCert, String algo, boolean useAlternativeProvider);
-
- List<EndpointCertificateRequest> listCertificates();
-
- void deleteCertificate(String requestId);
-
- EndpointCertificateDetails certificateDetails(String requestId);
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateProviderMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateProviderMock.java
deleted file mode 100644
index d73c6b53965..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateProviderMock.java
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-import java.time.Instant;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.UUID;
-
-/**
- * @author tokle
- * @author andreer
- */
-public class EndpointCertificateProviderMock implements EndpointCertificateProvider {
-
- private final Map<String, List<String>> dnsNames = new HashMap<>();
- private final Map<String, EndpointCertificate> certificates = new HashMap<>();
-
- public List<String> dnsNamesOf(String rootRequestId) {
- return Collections.unmodifiableList(dnsNames.getOrDefault(rootRequestId, List.of()));
- }
-
- @Override
- public EndpointCertificate requestCaSignedCertificate(String key, List<String> dnsNames, Optional<EndpointCertificate> currentCert, String algo, boolean useAlternativeProvider) {
- String endpointCertificatePrefix = "vespa.tls.%s".formatted(key);
- long epochSecond = Instant.now().getEpochSecond();
- long inAnHour = epochSecond + 3600;
- String requestId = UUID.randomUUID().toString();
- this.dnsNames.put(requestId, dnsNames);
- int version = currentCert.map(c -> currentCert.get().version() + 1).orElse(0);
- EndpointCertificate cert = new EndpointCertificate(endpointCertificatePrefix + "-key", endpointCertificatePrefix + "-cert", version, 0,
- currentCert.map(EndpointCertificate::rootRequestId).orElse(requestId), Optional.of(requestId), dnsNames, "mockCa", Optional.of(inAnHour), Optional.of(epochSecond), Optional.empty());
- currentCert.ifPresent(c -> certificates.remove(c.leafRequestId().orElseThrow()));
- certificates.put(requestId, cert);
- return cert;
- }
-
- @Override
- public List<EndpointCertificateRequest> listCertificates() {
- return certificates.values().stream()
- .map(p -> new EndpointCertificateRequest(
- p.leafRequestId().orElse(p.rootRequestId()),
- "requestor",
- "ticketId",
- "athenzDomain",
- p.requestedDnsSans().stream()
- .map(san -> new EndpointCertificateRequest.DnsNameStatus(san, "done"))
- .toList(),
- 3600,
- "ok",
- "2021-09-28T00:14:31.946562037Z",
- p.expiry().orElseThrow(),
- p.issuer(),
- "rsa_2048"
- ))
- .toList();
- }
-
- @Override
- public void deleteCertificate(String requestId) {
- dnsNames.remove(requestId);
- certificates.remove(requestId);
- }
-
- @Override
- public EndpointCertificateDetails certificateDetails(String requestId) {
- var request = certificates.get(requestId);
-
- if (request == null) throw new IllegalArgumentException("Unknown certificate request");
-
- return new EndpointCertificateDetails(requestId,
- "requestor",
- "ok",
- "ticket_id",
- "athenz_domain",
- request.requestedDnsSans().stream().map(name -> new EndpointCertificateRequest.DnsNameStatus(name, "done")).toList(),
- "duration_sec",
- "expiry",
- request.keyName(),
- request.keyName(),
- "0",
- request.certName(),
- request.certName(),
- "0",
- "2021-09-28T00:14:31.946562037Z",
- true,
- "public_key_algo",
- "issuer",
- "serial");
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateRequest.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateRequest.java
deleted file mode 100644
index 8d4514c5713..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateRequest.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-import java.util.List;
-
-/**
- * This class is used for details about an application's endpoint certificate received from the certificate provider.
- *
- * @param createTime ISO 8601
- * @author andreer
- */
-public record EndpointCertificateRequest(String requestId, String requestor, String ticketId, String athenzDomain,
- List<DnsNameStatus> dnsNames, long durationSec, String status,
- String createTime, long expiry, String issuer, String publicKeyAlgo) {
-
- public record DnsNameStatus(String dnsName, String status) {}
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidator.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidator.java
deleted file mode 100644
index c3b1c074b3c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidator.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.List;
-
-/**
- * @author andreer
- */
-public interface EndpointCertificateValidator {
- void validate(EndpointCertificate endpointCertificate, String serializedInstanceId, ZoneId zone, List<String> requiredNamesForZone);
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java
deleted file mode 100644
index 13fa6c862a7..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorImpl.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.secretstore.SecretNotFoundException;
-import com.yahoo.container.jdisc.secretstore.SecretStore;
-import com.yahoo.security.SubjectAlternativeName;
-import com.yahoo.security.X509CertificateUtils;
-
-import java.security.cert.X509Certificate;
-import java.time.Clock;
-import java.time.Instant;
-import java.util.List;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * @author andreer
- */
-public class EndpointCertificateValidatorImpl implements EndpointCertificateValidator {
- private final SecretStore secretStore;
- private final Clock clock;
-
- private static final Logger log = Logger.getLogger(EndpointCertificateValidator.class.getName());
-
- public EndpointCertificateValidatorImpl(SecretStore secretStore, Clock clock) {
- this.secretStore = secretStore;
- this.clock = clock;
- }
-
- @Override
- public void validate(EndpointCertificate endpointCertificate, String serializedInstanceId, ZoneId zone, List<String> requiredNamesForZone) {
- try {
- var pemEncodedEndpointCertificate = secretStore.getSecret(endpointCertificate.certName(), endpointCertificate.version());
-
- if (pemEncodedEndpointCertificate == null)
- throw new EndpointCertificateException(EndpointCertificateException.Type.CERT_NOT_AVAILABLE, "Secret store returned null for certificate");
-
- List<X509Certificate> x509CertificateList = X509CertificateUtils.certificateListFromPem(pemEncodedEndpointCertificate);
-
- if (x509CertificateList.isEmpty())
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Empty certificate list");
- if (x509CertificateList.size() < 2)
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Only a single certificate found in chain - intermediate certificates likely missing");
-
- Instant now = clock.instant();
- Instant firstExpiry = Instant.MAX;
- for (X509Certificate x509Certificate : x509CertificateList) {
- Instant notBefore = x509Certificate.getNotBefore().toInstant();
- Instant notAfter = x509Certificate.getNotAfter().toInstant();
- if (now.isBefore(notBefore))
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Certificate is not yet valid");
- if (now.isAfter(notAfter))
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Certificate has expired");
- if (notAfter.isBefore(firstExpiry)) firstExpiry = notAfter;
- }
-
- X509Certificate endEntityCertificate = x509CertificateList.get(0);
- Set<String> subjectAlternativeNames = X509CertificateUtils.getSubjectAlternativeNames(endEntityCertificate).stream()
- .filter(san -> san.getType().equals(SubjectAlternativeName.Type.DNS))
- .map(SubjectAlternativeName::getValue).collect(Collectors.toSet());
-
- if (!subjectAlternativeNames.containsAll(requiredNamesForZone))
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Certificate is missing required SANs for zone " + zone.value());
-
- } catch (SecretNotFoundException s) {
- // Normally because the cert is in the process of being provisioned - this will cause a retry in InternalStepRunner
- throw new EndpointCertificateException(EndpointCertificateException.Type.CERT_NOT_AVAILABLE, "Certificate not found in secret store", s);
- } catch (EndpointCertificateException e) {
- if (!e.type().equals(EndpointCertificateException.Type.CERT_NOT_AVAILABLE)) { // such failures are normal and will be retried, it takes some time to show up in the secret store
- log.log(Level.WARNING, "Certificate validation failure for " + serializedInstanceId, e);
- }
- throw e;
- } catch (Exception e) {
- log.log(Level.WARNING, "Certificate validation failure for " + serializedInstanceId, e);
- throw new EndpointCertificateException(EndpointCertificateException.Type.VERIFICATION_FAILURE, "Certificate validation failure for app " + serializedInstanceId, e);
- }
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorMock.java
deleted file mode 100644
index 594f5fd6b92..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateValidatorMock.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.List;
-
-/**
- * @author andreer
- */
-public class EndpointCertificateValidatorMock implements EndpointCertificateValidator {
-
- @Override
- public void validate(
- EndpointCertificate endpointCertificate,
- String serializedApplicationId,
- ZoneId zone,
- List<String> requiredNamesForZone) {
- // Mock does no validation - for unit tests only!
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/package-info.java
deleted file mode 100644
index ec5b54d62d6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Application.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Application.java
deleted file mode 100644
index c0926472359..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Application.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/**
- * The client-side version of the node repository's view of applications
- *
- * @author bratseth
- */
-public class Application {
-
- private final ApplicationId id;
- private final Map<ClusterSpec.Id, Cluster> clusters;
-
- public Application(ApplicationId id, Collection<Cluster> clusters) {
- this.id = id;
- this.clusters = clusters.stream().collect(Collectors.toMap(c -> c.id(), c -> c));
- }
-
- public ApplicationId id() { return id; }
-
- public Map<ClusterSpec.Id, Cluster> clusters() { return clusters; }
-
- @Override
- public String toString() {
- return "application '" + id + "'";
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ApplicationReindexing.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ApplicationReindexing.java
deleted file mode 100644
index 3f341a8db9b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ApplicationReindexing.java
+++ /dev/null
@@ -1,176 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import java.time.Instant;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Reindexing status for a single Vespa application.
- *
- * @author jonmv
- */
-public class ApplicationReindexing {
-
- private final boolean enabled;
- private final Map<String, Cluster> clusters;
-
- public ApplicationReindexing(boolean enabled, Map<String, Cluster> clusters) {
- this.enabled = enabled;
- this.clusters = Map.copyOf(clusters);
- }
-
- public boolean enabled() {
- return enabled;
- }
-
- public Map<String, Cluster> clusters() {
- return clusters;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ApplicationReindexing that = (ApplicationReindexing) o;
- return enabled == that.enabled &&
- clusters.equals(that.clusters);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(enabled, clusters);
- }
-
- @Override
- public String toString() {
- return "ApplicationReindexing{" +
- "enabled=" + enabled +
- ", clusters=" + clusters +
- '}';
- }
-
-
- public static class Cluster {
-
- private final Map<String, Long> pending;
- private final Map<String, Status> ready;
-
- public Cluster(Map<String, Long> pending, Map<String, Status> ready) {
- this.pending = Map.copyOf(pending);
- this.ready = Map.copyOf(ready);
- }
-
- public Map<String, Long> pending() {
- return pending;
- }
-
- public Map<String, Status> ready() {
- return ready;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Cluster cluster = (Cluster) o;
- return pending.equals(cluster.pending) &&
- ready.equals(cluster.ready);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(pending, ready);
- }
-
- @Override
- public String toString() {
- return "Cluster{" +
- "pending=" + pending +
- ", ready=" + ready +
- '}';
- }
-
- }
-
-
- public static class Status {
-
- private final Instant readyAt;
- private final Instant startedAt;
- private final Instant endedAt;
- private final State state;
- private final String message;
- private final Double progress;
- private final Double speed;
- private final String cause;
-
- public Status(Instant readyAt, Instant startedAt, Instant endedAt, State state, String message, Double progress, Double speed, String cause) {
- this.readyAt = readyAt;
- this.startedAt = startedAt;
- this.endedAt = endedAt;
- this.state = state;
- this.message = message;
- this.progress = progress;
- this.speed = speed;
- this.cause = cause;
- }
-
- public Status(Instant readyAt) {
- this(readyAt, null, null, null, null, null, 1.0, null);
- }
-
- public Optional<Instant> readyAt() { return Optional.ofNullable(readyAt); }
- public Optional<Instant> startedAt() { return Optional.ofNullable(startedAt); }
- public Optional<Instant> endedAt() { return Optional.ofNullable(endedAt); }
- public Optional<State> state() { return Optional.ofNullable(state); }
- public Optional<String> message() { return Optional.ofNullable(message); }
- public Optional<Double> progress() { return Optional.ofNullable(progress); }
- public Optional<Double> speed() { return Optional.ofNullable(speed); }
- public Optional<String> cause() { return Optional.ofNullable(cause); }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Status status = (Status) o;
- return Objects.equals(readyAt, status.readyAt) &&
- Objects.equals(startedAt, status.startedAt) &&
- Objects.equals(endedAt, status.endedAt) &&
- state == status.state &&
- Objects.equals(message, status.message) &&
- Objects.equals(progress, status.progress) &&
- Objects.equals(speed, status.speed) &&
- Objects.equals(cause, status.cause);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(readyAt, startedAt, endedAt, state, message, progress, speed, cause);
- }
-
- @Override
- public String toString() {
- return "Status{" +
- "readyAt=" + readyAt +
- ", startedAt=" + startedAt +
- ", endedAt=" + endedAt +
- ", state=" + state +
- ", message='" + message + '\'' +
- ", progress=" + progress +
- ", speed=" + speed +
- ", cause='" + cause + '\'' +
- '}';
- }
-
- }
-
-
- public enum State {
-
- PENDING, RUNNING, FAILED, SUCCESSFUL;
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ApplicationStats.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ApplicationStats.java
deleted file mode 100644
index 25758031360..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ApplicationStats.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.config.provision.ApplicationId;
-
-/**
- * @author bratseth
- */
-public class ApplicationStats {
-
- private final ApplicationId id;
- private final Load load;
- private final double cost;
- private final double unutilizedCost;
-
- public ApplicationStats(ApplicationId id, Load load, double cost, double unutilizedCost) {
- this.id = id;
- this.load = load;
- this.cost = cost;
- this.unutilizedCost = unutilizedCost;
- }
-
- public ApplicationId id() { return id; }
- public Load load() { return load; }
- public double cost() { return cost; }
- public double unutilizedCost() { return unutilizedCost; }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ArchiveUris.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ArchiveUris.java
deleted file mode 100644
index 1203c171600..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ArchiveUris.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.TenantName;
-
-import java.net.URI;
-import java.util.Map;
-
-/**
- * @author freva
- */
-public record ArchiveUris(Map<TenantName, URI> tenantArchiveUris, Map<CloudAccount, URI> accountArchiveUris) {
- public static final ArchiveUris EMPTY = new ArchiveUris(Map.of(), Map.of());
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Cluster.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Cluster.java
deleted file mode 100644
index c1ac4f0316f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Cluster.java
+++ /dev/null
@@ -1,198 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.config.provision.ClusterResources;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.IntRange;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * @author bratseth
- */
-public class Cluster {
-
- private final ClusterSpec.Id id;
- private final ClusterSpec.Type type;
- private final ClusterResources min;
- private final ClusterResources max;
- private final IntRange groupSize;
- private final ClusterResources current;
- private final Autoscaling target;
- private final Autoscaling suggested;
- private final List<ScalingEvent> scalingEvents;
- private final Duration scalingDuration;
-
- public Cluster(ClusterSpec.Id id,
- ClusterSpec.Type type,
- ClusterResources min,
- ClusterResources max,
- IntRange groupSize,
- ClusterResources current,
- Autoscaling target,
- Autoscaling suggested,
- List<ScalingEvent> scalingEvents,
- Duration scalingDuration) {
- this.id = id;
- this.type = type;
- this.min = min;
- this.max = max;
- this.groupSize = groupSize;
- this.current = current;
- this.target = target;
- this.suggested = suggested;
- this.scalingEvents = scalingEvents;
- this.scalingDuration = scalingDuration;
- }
-
- public ClusterSpec.Id id() { return id; }
-
- public ClusterSpec.Type type() { return type; }
-
- public ClusterResources min() { return min; }
-
- public ClusterResources max() { return max; }
-
- public IntRange groupSize() { return groupSize; }
-
- public ClusterResources current() { return current; }
-
- public Autoscaling target() { return target; }
-
- public Autoscaling suggested() { return suggested; }
-
- public List<ScalingEvent> scalingEvents() { return scalingEvents; }
-
- public Duration scalingDuration() { return scalingDuration; }
-
- @Override
- public String toString() {
- return id.toString();
- }
-
- public static class ScalingEvent {
-
- private final ClusterResources from, to;
- private final Instant at;
- private final Optional<Instant> completion;
-
- public ScalingEvent(ClusterResources from, ClusterResources to, Instant at, Optional<Instant> completion) {
- this.from = from;
- this.to = to;
- this.at = at;
- this.completion = completion;
- }
-
- public ClusterResources from() {return from;}
-
- public ClusterResources to() {return to;}
-
- public Instant at() {return at;}
-
- public Optional<Instant> completion() {return completion;}
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ScalingEvent that = (ScalingEvent) o;
- return Objects.equals(from, that.from) && Objects.equals(to, that.to) && Objects.equals(at, that.at) && Objects.equals(completion, that.completion);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(from, to, at, completion);
- }
-
- @Override
- public String toString() {
- return "ScalingEvent{" +
- "from=" + from +
- ", to=" + to +
- ", at=" + at +
- ", completion=" + completion +
- '}';
- }
- }
-
- public static class Autoscaling {
-
- private final String status;
- private final String description;
- private final Optional<ClusterResources> resources;
- private final Instant at;
- private final Load peak;
- private final Load ideal;
- private final Metrics metrics;
-
- public Autoscaling(String status, String description, Optional<ClusterResources> resources, Instant at,
- Load peak, Load ideal, Metrics metrics) {
- this.status = status;
- this.description = description;
- this.resources = resources;
- this.at = at;
- this.peak = peak;
- this.ideal = ideal;
- this.metrics = metrics;
- }
-
- public String status() {return status;}
- public String description() {return description;}
- public Optional<ClusterResources> resources() {
- return resources;
- }
- public Instant at() {return at;}
- public Load peak() {return peak;}
- public Load ideal() {return ideal;}
- public Metrics metrics() { return metrics; }
-
- @Override
- public boolean equals(Object o) {
- if (!(o instanceof Autoscaling other)) return false;
- if (!this.status.equals(other.status)) return false;
- if (!this.description.equals(other.description)) return false;
- if (!this.resources.equals(other.resources)) return false;
- if (!this.at.equals(other.at)) return false;
- if (!this.peak.equals(other.peak)) return false;
- if (!this.ideal.equals(other.ideal)) return false;
- if (!this.metrics.equals(other.metrics)) return false;
- return true;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(status, description, at, peak, ideal);
- }
-
- @Override
- public String toString() {
- return (resources.isPresent() ? "Autoscaling to " + resources : "Don't autoscale") +
- (description.isEmpty() ? "" : ": " + description);
- }
-
- public static Autoscaling empty() {
- return new Autoscaling("unavailable",
- "",
- Optional.empty(),
- Instant.EPOCH,
- Load.zero(),
- Load.zero(),
- Metrics.zero());
- }
-
- // Used to create BcpGroupInfo
- public record Metrics(double queryRate, double growthRateHeadroom, double cpuCostPerQuery) {
-
- public static Metrics zero() {
- return new Metrics(0, 0, 0);
- }
-
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
deleted file mode 100644
index 524c723157f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServer.java
+++ /dev/null
@@ -1,167 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import ai.vespa.http.HttpURL.Query;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.EndpointsChecker.Availability;
-import com.yahoo.config.provision.EndpointsChecker.Endpoint;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-import ai.vespa.http.DomainName;
-import ai.vespa.http.HttpURL.Path;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeploymentData;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.SearchNodeMetrics;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.FingerPrint;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
-import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-
-import java.io.InputStream;
-import java.net.URI;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * The API controllers use when communicating with config servers.
- *
- * @author Øyvind Grønnesby
- */
-public interface ConfigServer {
-
- interface PreparedApplication {
- DeploymentResult deploymentResult();
- }
-
- PreparedApplication deploy(DeploymentData deployment);
-
- void reindex(DeploymentId deployment, List<String> clusterNames, List<String> documentTypes, boolean indexedOnly, Double speed, String cause);
-
- ApplicationReindexing getReindexing(DeploymentId deployment);
-
- void disableReindexing(DeploymentId deployment);
-
- void enableReindexing(DeploymentId deployment);
-
- void restart(DeploymentId deployment, RestartFilter restartFilter);
-
- void deactivate(DeploymentId deployment);
-
- boolean isSuspended(DeploymentId deployment);
-
- /** Returns a proxied response from a given path running on a given service and node */
- ProxyResponse getServiceNodePage(DeploymentId deployment, String serviceName, DomainName node, Path subPath, Query query);
-
- /** Returns health status for the services of an application */
- ProxyResponse getServiceNodes(DeploymentId deployment);
-
- /**
- * Gets the Vespa logs of the given deployment.
- *
- * If the "from" and/or "to" query parameters are present, they are read as millis since EPOCH, and used
- * to limit the time window for which log entries are gathered. <em>This is not exact, and will return too much.</em>
- * If the "hostname" query parameter is present, it limits the entries to be from that host.
- */
- InputStream getLogs(DeploymentId deployment, Map<String, String> queryParameters);
-
- /**
- * Gets the contents of a file inside the current application package for a given deployment. If the path is to
- * a directory, a JSON list with URLs to contents is returned.
- *
- * @param deployment deployment to get application package content for
- * @param path path within package to get
- * @param requestUri request URI on the controller, used to rewrite paths in response from config server
- */
- ProxyResponse getApplicationPackageContent(DeploymentId deployment, Path path, URI requestUri);
-
- List<ClusterMetrics> getDeploymentMetrics(DeploymentId deployment);
-
- List<SearchNodeMetrics> getSearchNodeMetrics(DeploymentId deployment);
-
- List<String> getContentClusters(DeploymentId deployment);
-
- /**
- * Set new status for a endpoint of a single deployment.
- *
- * @param deployment The deployment to change
- * @param upstreamNames The upstream names to modify. Upstream name is a unique identifier for the routing status
- * of a cluster in a deployment
- * @param status The new status
- */
- void setGlobalRotationStatus(DeploymentId deployment, List<String> upstreamNames, EndpointStatus status);
-
- /**
- * Set the new status for an entire zone.
- *
- * @param zone the zone
- * @param in whether to set zone status to 'in' or 'out'
- */
- void setGlobalRotationStatus(ZoneId zone, boolean in);
-
- /**
- * Get the endpoint status for an app in one zone.
- *
- * @param deployment The deployment to change
- * @param upstreamName The upstream to query. Upstream name is a unique identifier for the global route of a
- * deployment in the shared routing layer
- * @return The endpoint status with metadata
- */
- EndpointStatus getGlobalRotationStatus(DeploymentId deployment, String upstreamName);
-
- /**
- * Get the status for an entire zone.
- *
- * @param zone the zone
- * @return whether the zone status is 'in'
- */
- boolean getGlobalRotationStatus(ZoneId zone);
-
- /** The node repository on this config server */
- NodeRepository nodeRepository();
-
- /** Get service convergence status for given deployment, using the nodes in the model at the given Vespa version. */
- Optional<ServiceConvergence> serviceConvergence(DeploymentId deployment, Optional<Version> version);
-
- /** Get all load balancers for application in given zone */
- List<LoadBalancer> getLoadBalancers(ApplicationId application, ZoneId zone);
-
- /** List all flag data for the given zone */
- List<FlagData> listFlagData(ZoneId zone);
-
- /** Gets status for tester application */
- TesterCloud.Status getTesterStatus(DeploymentId deployment);
-
- /** Starts tests on tester node */
- String startTests(DeploymentId deployment, TesterCloud.Suite suite, byte[] config);
-
- /** Gets log from tester node */
- List<LogEntry> getTesterLog(DeploymentId deployment, long after);
-
- /** Is tester node ready */
- boolean isTesterReady(DeploymentId deployment);
-
- Optional<TestReport> getTestReport(DeploymentId deployment);
-
- Availability verifyEndpoints(DeploymentId deploymentId, List<Endpoint> zoneEndpoints);
-
- /** Get maximum resources consumed */
- QuotaUsage getQuotaUsage(DeploymentId deploymentId);
-
- /** Sets suspension status — whether application node operations are orchestrated — for the given deployment. */
- void setSuspension(DeploymentId deploymentId, boolean suspend);
-
- /** Validates secret store configuration. */
- String validateSecretStore(DeploymentId deploymentId, TenantSecretStore tenantSecretStore, String region, String parameterName);
-
- /** Fingerprints of active data plane tokens, per healthy host with token auth, in the given deployment. */
- Map<HostName, Map<TokenId, List<FingerPrint>>> activeTokenFingerprints(DeploymentId deploymentId);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServerException.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServerException.java
deleted file mode 100644
index 66361404712..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServerException.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.SlimeUtils;
-
-import java.util.stream.Stream;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * An exception due to server error, a bad request, or similar.
- *
- * @author jonmv
- */
-public class ConfigServerException extends RuntimeException {
-
- private final ErrorCode code;
- private final String message;
-
- public ConfigServerException(ErrorCode code, String message) {
- super(message);
- this.code = code;
- this.message = message;
- }
-
- public ConfigServerException(ErrorCode code, String message, String context) {
- super(context + ": " + message);
- this.code = code;
- this.message = message;
- }
-
- public ErrorCode code() { return code; }
-
- public String message() { return message; }
-
- public enum ErrorCode {
- APPLICATION_LOCK_FAILURE,
- BAD_REQUEST,
- ACTIVATION_CONFLICT,
- INTERNAL_SERVER_ERROR,
- INVALID_APPLICATION_PACKAGE,
- METHOD_NOT_ALLOWED,
- NOT_FOUND,
- NODE_ALLOCATION_FAILURE,
- REQUEST_TIMEOUT,
- UNKNOWN_VESPA_VERSION,
- PARENT_HOST_NOT_READY,
- CERTIFICATE_NOT_READY,
- LOAD_BALANCER_NOT_READY,
- INCOMPLETE_RESPONSE,
- CONFIG_NOT_CONVERGED,
- QUOTA_EXCEEDED
- }
-
- // Note: Used by code in internal repo
- public static ConfigServerException readException(byte[] body, String context) {
- Inspector root = SlimeUtils.jsonToSlime(body).get();
- String codeName = root.field("error-code").asString();
- ErrorCode code = Stream.of(ErrorCode.values())
- .filter(value -> value.name().equals(codeName))
- .findAny().orElse(ErrorCode.INCOMPLETE_RESPONSE);
- String message = root.field("message").valid() ? root.field("message").asString() : new String(body, UTF_8);
- return new ConfigServerException(code, message, context);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServerVersion.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServerVersion.java
deleted file mode 100644
index 6b637145734..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ConfigServerVersion.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.component.Version;
-
-/**
- * Represents the config server's current and wanted version.
- *
- * @author mpolden
- */
-public class ConfigServerVersion {
-
- private final Version current;
- private final Version wanted;
-
- public ConfigServerVersion(Version current, Version wanted) {
- this.current = current;
- this.wanted = wanted;
- }
-
- public Version current() {
- return current;
- }
-
- public Version wanted() {
- return wanted;
- }
-
- public boolean upgrading() {
- return !current.equals(wanted);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ContainerEndpoint.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ContainerEndpoint.java
deleted file mode 100644
index fc0dd2f40b0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ContainerEndpoint.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.config.provision.zone.RoutingMethod;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.OptionalInt;
-
-/**
- * The endpoint of a container cluster. This encapsulates the endpoint details passed from controller to the config
- * server on deploy.
- *
- * @param clusterId ID of the cluster to which this points
- * @param scope Scope of this endpoint
- * @param names All valid DNS names for this endpoint. This can contain both proper DNS names and synthetic identifiers
- * used for routing, such as a Host header value that is not necessarily a proper DNS name
- * @param weight The relative weight of this endpoint
- * @param routingMethod The routing method used by this endpoint
- * @param authMethod The authentication method supported by this endpoint
- *
- * @author mpolden
- */
-public record ContainerEndpoint(String clusterId, String scope, List<String> names, OptionalInt weight,
- RoutingMethod routingMethod, AuthMethod authMethod) {
-
- public ContainerEndpoint(String clusterId, String scope, List<String> names, OptionalInt weight,
- RoutingMethod routingMethod, AuthMethod authMethod) {
- this.clusterId = nonEmpty(clusterId, "clusterId must be non-empty");
- this.scope = Objects.requireNonNull(scope, "scope must be non-null");
- this.names = List.copyOf(Objects.requireNonNull(names, "names must be non-null"));
- this.weight = Objects.requireNonNull(weight, "weight must be non-null");
- this.routingMethod = Objects.requireNonNull(routingMethod, "routingMethod must be non-null");
- this.authMethod = Objects.requireNonNull(authMethod, "authMethod must be non-null");
- }
-
- private static String nonEmpty(String s, String message) {
- if (s == null || s.isBlank()) throw new IllegalArgumentException(message);
- return s;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/DeploymentResult.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/DeploymentResult.java
deleted file mode 100644
index e73d783c530..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/DeploymentResult.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import java.util.List;
-import java.util.logging.Level;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * The result of a deployment, carried out against a {@link ConfigServer}.
- *
- * @author jonmv
- */
-public record DeploymentResult(String message, List<LogEntry> log) {
-
- public DeploymentResult {
- requireNonNull(message);
- requireNonNull(log);
- }
-
- public record LogEntry(long epochMillis, String message, Level level, boolean concernsPackage) {
-
- public LogEntry {
- requireNonNull(message);
- requireNonNull(level);
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/FlagsV1Api.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/FlagsV1Api.java
deleted file mode 100644
index 357f0d993b6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/FlagsV1Api.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.vespa.flags.json.wire.WireFlagData;
-import com.yahoo.vespa.flags.json.wire.WireFlagDataList;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.GET;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import java.util.List;
-import java.util.Map;
-
-/**
- * @author hakonhall
- */
-@Path("")
-@Produces(MediaType.APPLICATION_JSON)
-@Consumes(MediaType.APPLICATION_JSON)
-public interface FlagsV1Api {
- @PUT
- @Path("/data/{flagId}")
- void putFlagData(@PathParam("flagId") String flagId, @QueryParam("force") Boolean force, WireFlagData flagData);
-
- @GET
- @Path("/data/{flagId}")
- WireFlagData getFlagData(@PathParam("flagId") String flagId, @QueryParam("force") Boolean force);
-
- @DELETE
- @Path("/data/{flagId}")
- void deleteFlagData(@PathParam("flagId") String flagId, @QueryParam("force") Boolean force);
-
- @GET
- @Path("/data")
- WireFlagDataList listFlagData(@QueryParam("recursive") Boolean recursive);
-
- @GET
- @Path("/defined")
- Map<String, WireFlagDefinition> listFlagDefinition();
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- @JsonInclude(JsonInclude.Include.NON_NULL)
- class WireFlagDefinition {
- @JsonProperty("owners") public List<String> owners;
- @JsonProperty("expiresAt") public String expiresAt;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Load.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Load.java
deleted file mode 100644
index 4ed1f92c4a4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Load.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import java.util.Objects;
-
-/**
- * @author bratseth
- */
-public class Load {
-
- private final double cpu;
- private final double memory;
- private final double disk;
-
- public Load(double cpu, double memory, double disk) {
- this.cpu = cpu;
- this.memory = memory;
- this.disk = disk;
- }
-
- public double cpu() { return cpu; }
- public double memory() { return memory; }
- public double disk() { return disk; }
-
- @Override
- public String toString() {
- return "load: cpu " + cpu + ", memory " + memory + ", disk " + disk;
- }
-
- @Override
- public int hashCode() { return Objects.hash(cpu, memory, disk); }
-
- @Override
- public boolean equals(Object o) {
- if ( ! (o instanceof Load other)) return false;
- return cpu == other.cpu && memory == other.memory && disk == other.disk;
- }
-
- public static Load zero() { return new Load(0, 0, 0); }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java
deleted file mode 100644
index ce9976a3c7e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/LoadBalancer.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import ai.vespa.http.DomainName;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
-
-import java.util.List;
-import java.util.Optional;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Represents an exclusive load balancer, assigned to an application's cluster.
- *
- * @author mortent
- */
-public record LoadBalancer(String id, ApplicationId application, ClusterSpec.Id cluster,
- Optional<DomainName> hostname, Optional<String> ipAddress,
- State state, Optional<String> dnsZone, Optional<CloudAccount> cloudAccount,
- Optional<PrivateServiceInfo> service, boolean isPublic) {
-
- public LoadBalancer {
- requireNonNull(id, "id must be non-null");
- requireNonNull(application, "application must be non-null");
- requireNonNull(cluster, "cluster must be non-null");
- requireNonNull(hostname, "hostname must be non-null");
- requireNonNull(ipAddress, "ipAddress must be non-null");
- requireNonNull(state, "state must be non-null");
- requireNonNull(dnsZone, "dnsZone must be non-null");
- requireNonNull(cloudAccount, "cloudAccount must be non-null");
- requireNonNull(service, "service must be non-null");
- }
-
- public enum State {
- active,
- inactive,
- reserved,
- unknown
- }
-
- public record PrivateServiceInfo(String id, List<AllowedUrn> allowedUrns) { }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Log.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Log.java
deleted file mode 100644
index dcdd9e8ec87..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Log.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-
-/**
- * @author Tony Vaagenes
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class Log {
- public long time;
- public String level;
- public String message;
- public boolean applicationPackage;
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Node.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Node.java
deleted file mode 100644
index f4706fca27c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/Node.java
+++ /dev/null
@@ -1,812 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.DockerImage;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.TenantName;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.UUID;
-
-/**
- * A node in hosted Vespa.
- *
- * This is immutable and all fields are guaranteed to be non-null. This should never leak any wire format types or
- * types from third-party libraries.
- *
- * Use {@link Node#builder()} or {@link Node#builder(Node)} to create instances of this.
- *
- * @author mpolden
- * @author jonmv
- */
-public class Node {
-
- private final String id;
- private final HostName hostname;
- private final Optional<HostName> parentHostname;
- private final State state;
- private final NodeType type;
- private final NodeResources resources;
- private final Optional<ApplicationId> owner;
- private final Version currentVersion;
- private final Version wantedVersion;
- private final Version currentOsVersion;
- private final Version wantedOsVersion;
- private final boolean deferOsUpgrade;
- private final DockerImage currentDockerImage;
- private final DockerImage wantedDockerImage;
- private final ServiceState serviceState;
- private final Optional<Instant> suspendedSince;
- private final Optional<Instant> currentFirmwareCheck;
- private final Optional<Instant> wantedFirmwareCheck;
- private final long restartGeneration;
- private final long wantedRestartGeneration;
- private final long rebootGeneration;
- private final long wantedRebootGeneration;
- private final int cost;
- private final int failCount;
- private final Optional<String> flavor;
- private final String clusterId;
- private final ClusterType clusterType;
- private final String group;
- private final int index;
- private final boolean retired;
- private final boolean wantToRetire;
- private final boolean wantToDeprovision;
- private final boolean wantToRebuild;
- private final boolean down;
- private final Optional<TenantName> reservedTo;
- private final Optional<ApplicationId> exclusiveTo;
- private final Optional<ClusterType> exclusiveToClusterType;
- private final Map<String, String> reports;
- private final List<Event> history;
- private final List<String> ipAddresses;
- private final List<String> additionalIpAddresses;
- private final List<String> additionalHostnames;
- private final Optional<String> switchHostname;
- private final Optional<String> modelName;
- private final Environment environment;
- private final CloudAccount cloudAccount;
-
- private Node(String id, HostName hostname, Optional<HostName> parentHostname, State state, NodeType type,
- NodeResources resources, Optional<ApplicationId> owner, Version currentVersion, Version wantedVersion,
- Version currentOsVersion, Version wantedOsVersion, boolean deferOsUpgrade, Optional<Instant> currentFirmwareCheck,
- Optional<Instant> wantedFirmwareCheck, ServiceState serviceState, Optional<Instant> suspendedSince,
- long restartGeneration, long wantedRestartGeneration, long rebootGeneration,
- long wantedRebootGeneration, int cost, int failCount, Optional<String> flavor, String clusterId,
- ClusterType clusterType, String group, int index, boolean retired, boolean wantToRetire, boolean wantToDeprovision,
- boolean wantToRebuild, boolean down, Optional<TenantName> reservedTo, Optional<ApplicationId> exclusiveTo,
- DockerImage wantedDockerImage, DockerImage currentDockerImage, Optional<ClusterType> exclusiveToClusterType, Map<String, String> reports,
- List<Event> history, List<String> ipAddresses, List<String> additionalIpAddresses,
- List<String> additionalHostnames, Optional<String> switchHostname,
- Optional<String> modelName, Environment environment, CloudAccount cloudAccount) {
- this.id = Objects.requireNonNull(id, "id must be non-null");
- this.hostname = Objects.requireNonNull(hostname, "hostname must be non-null");
- this.parentHostname = Objects.requireNonNull(parentHostname, "parentHostname must be non-null");
- this.state = Objects.requireNonNull(state, "state must be non-null");
- this.type = Objects.requireNonNull(type, "type must be non-null");
- this.resources = Objects.requireNonNull(resources, "resources must be non-null");
- this.owner = Objects.requireNonNull(owner, "owner must be non-null");
- this.currentVersion = Objects.requireNonNull(currentVersion, "currentVersion must be non-null");
- this.wantedVersion = Objects.requireNonNull(wantedVersion, "wantedVersion must be non-null");
- this.currentOsVersion = Objects.requireNonNull(currentOsVersion, "currentOsVersion must be non-null");
- this.wantedOsVersion = Objects.requireNonNull(wantedOsVersion, "wantedOsVersion must be non-null");
- this.deferOsUpgrade = deferOsUpgrade;
- this.currentFirmwareCheck = Objects.requireNonNull(currentFirmwareCheck, "currentFirmwareCheck must be non-null");
- this.wantedFirmwareCheck = Objects.requireNonNull(wantedFirmwareCheck, "wantedFirmwareCheck must be non-null");
- this.serviceState = Objects.requireNonNull(serviceState, "serviceState must be non-null");
- this.suspendedSince = Objects.requireNonNull(suspendedSince, "suspendedSince must be non-null");
- this.restartGeneration = restartGeneration;
- this.wantedRestartGeneration = wantedRestartGeneration;
- this.rebootGeneration = rebootGeneration;
- this.wantedRebootGeneration = wantedRebootGeneration;
- this.cost = cost;
- this.failCount = failCount;
- this.flavor = Objects.requireNonNull(flavor, "flavor must be non-null");
- this.clusterId = Objects.requireNonNull(clusterId, "clusterId must be non-null");
- this.clusterType = Objects.requireNonNull(clusterType, "clusterType must be non-null");
- this.retired = retired;
- this.group = Objects.requireNonNull(group, "group must be non-null");
- this.index = index;
- this.wantToRetire = wantToRetire;
- this.wantToDeprovision = wantToDeprovision;
- this.reservedTo = Objects.requireNonNull(reservedTo, "reservedTo must be non-null");
- this.exclusiveToClusterType = Objects.requireNonNull(exclusiveToClusterType, "exclusiveToClusterType");
- this.exclusiveTo = Objects.requireNonNull(exclusiveTo, "exclusiveTo must be non-null");
- this.wantedDockerImage = Objects.requireNonNull(wantedDockerImage, "wantedDockerImage must be non-null");
- this.currentDockerImage = Objects.requireNonNull(currentDockerImage, "currentDockerImage must be non-null");
- this.wantToRebuild = wantToRebuild;
- this.down = down;
- this.reports = Map.copyOf(Objects.requireNonNull(reports, "reports must be non-null"));
- this.history = List.copyOf(Objects.requireNonNull(history, "history must be non-null"));
- this.ipAddresses = List.copyOf(Objects.requireNonNull(ipAddresses, "ipAddresses must be non-null"));
- this.additionalIpAddresses = List.copyOf(Objects.requireNonNull(additionalIpAddresses, "additionalIpAddresses must be non-null"));
- this.additionalHostnames = List.copyOf(Objects.requireNonNull(additionalHostnames, "additionalHostnames must be non-null"));
- this.switchHostname = Objects.requireNonNull(switchHostname, "switchHostname must be non-null");
- this.modelName = Objects.requireNonNull(modelName, "modelName must be non-null");
- this.environment = Objects.requireNonNull(environment, "environment must be non-ull");
- this.cloudAccount = Objects.requireNonNull(cloudAccount, "cloudAccount must be non-null");
- }
-
- /** The cloud provider's unique ID for this */
- public String id() {
- return id;
- }
-
- /** The hostname of this */
- public HostName hostname() {
- return hostname;
- }
-
- /** The parent hostname of this, if any */
- public Optional<HostName> parentHostname() {
- return parentHostname;
- }
-
- /** Current state of this */
- public State state() { return state; }
-
- /** The node type of this */
- public NodeType type() {
- return type;
- }
-
- /** Resources, such as CPU and memory, of this */
- public NodeResources resources() {
- return resources;
- }
-
- /** The application owning this, if any */
- public Optional<ApplicationId> owner() {
- return owner;
- }
-
- /** The Vespa version this is currently running */
- public Version currentVersion() {
- return currentVersion;
- }
-
- /** The wanted Vespa version */
- public Version wantedVersion() {
- return wantedVersion;
- }
-
- /** The OS version this is currently running */
- public Version currentOsVersion() {
- return currentOsVersion;
- }
-
- /** The wanted OS version */
- public Version wantedOsVersion() {
- return wantedOsVersion;
- }
-
- /** Returns whether the node is currently deferring any OS upgrade */
- public boolean deferOsUpgrade() {
- return deferOsUpgrade;
- }
-
- /** The container image of this is currently running */
- public DockerImage currentDockerImage() {
- return currentDockerImage;
- }
-
- /** The wanted Docker image */
- public DockerImage wantedDockerImage() {
- return wantedDockerImage;
- }
-
- /** The last time this checked for a firmware update */
- public Optional<Instant> currentFirmwareCheck() {
- return currentFirmwareCheck;
- }
-
- /** The wanted time this should check for a firmware update */
- public Optional<Instant> wantedFirmwareCheck() {
- return wantedFirmwareCheck;
- }
-
- /** The current service state of this */
- public ServiceState serviceState() {
- return serviceState;
- }
-
- /** The most recent time this suspended, if any */
- public Optional<Instant> suspendedSince() {
- return suspendedSince;
- }
-
- /** The current restart generation */
- public long restartGeneration() {
- return restartGeneration;
- }
-
- /** The wanted restart generation */
- public long wantedRestartGeneration() {
- return wantedRestartGeneration;
- }
-
- /** The current reboot generation */
- public long rebootGeneration() {
- return rebootGeneration;
- }
-
- /** The wanted reboot generation */
- public long wantedRebootGeneration() {
- return wantedRebootGeneration;
- }
-
- /** A number representing the cost of this */
- public int cost() {
- return cost;
- }
-
- /** How many times this has failed */
- public int failCount() {
- return failCount;
- }
-
- /** The flavor of this */
- public Optional<String> flavor() {
- return flavor;
- }
-
- /** The cluster ID of this, empty string if unallocated */
- public String clusterId() {
- return clusterId;
- }
-
- /** The cluster type of this */
- public ClusterType clusterType() {
- return clusterType;
- }
-
- /** Whether this is retired */
- public boolean retired() {
- return retired;
- }
-
- /** The group of this node, empty string if unallocated */
- public String group() { return group; }
-
- /** The membership index of this node */
- public int index() { return index; }
-
- /** Whether this node has been requested to retire */
- public boolean wantToRetire() {
- return wantToRetire;
- }
-
- /** Whether this node has been requested to deprovision */
- public boolean wantToDeprovision() {
- return wantToDeprovision;
- }
-
- /** Whether this node has been requested to rebuild */
- public boolean wantToRebuild() {
- return wantToRebuild;
- }
-
- /** Whether this node is currently down */
- public boolean down() { return down; }
-
- /** The tenant this has been reserved to, if any */
- public Optional<TenantName> reservedTo() { return reservedTo; }
-
- /** The application this has been provisioned exclusively for, if any */
- public Optional<ApplicationId> exclusiveTo() { return exclusiveTo; }
-
- /** The cluster type this has been provisioned exclusively for, if any */
- public Optional<ClusterType> exclusiveToClusterType() { return exclusiveToClusterType; }
-
- /** Returns the reports of this node. Key is the report ID. Value is untyped, but is typically a JSON string */
- public Map<String, String> reports() {
- return reports;
- }
-
- /** History of events affecting this */
- public List<Event> history() {
- return history;
- }
-
- /** IP addresses of this */
- public List<String> ipAddresses() {
- return ipAddresses;
- }
-
- /** Additional IP addresses available on this, usable by child nodes */
- public List<String> additionalIpAddresses() {
- return additionalIpAddresses;
- }
-
- /** Additional hostnames available on this, usable by child nodes */
- public List<String> additionalHostnames() {
- return additionalHostnames;
- }
-
- /** Hostname of the switch this is connected to, if any */
- public Optional<String> switchHostname() {
- return switchHostname;
- }
-
- /** The server model of this, if any */
- public Optional<String> modelName() { return modelName; }
-
- /** The environment this runs in */
- public Environment environment() {
- return environment;
- }
-
- public CloudAccount cloudAccount() {
- return cloudAccount;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Node node = (Node) o;
- return Objects.equals(hostname, node.hostname);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(hostname);
- }
-
- /** Known node states */
- public enum State {
- provisioned,
- ready,
- reserved,
- active,
- inactive,
- dirty,
- failed,
- parked,
- breakfixed,
- deprovisioned,
- unknown
- }
-
- /** Known node states with regards to service orchestration */
- public enum ServiceState {
- expectedUp,
- allowedDown,
- permanentlyDown,
- unorchestrated,
- unknown
- }
-
- /** Known cluster types. */
- public enum ClusterType {
- admin,
- container,
- content,
- combined,
- unknown
- }
-
- /** Known nope environments */
- public enum Environment {
- bareMetal,
- virtualMachine,
- dockerContainer,
- unknown,
- }
-
- /** A node event */
- public static class Event {
-
- private final Instant at;
- private final String agent;
- private final String name;
-
- public Event(Instant at, String agent, String name) {
- this.at = Objects.requireNonNull(at);
- this.agent = Objects.requireNonNull(agent);
- this.name = Objects.requireNonNull(name);
- }
-
- /** The time this occurred */
- public Instant at() {
- return at;
- }
-
- /** The agent responsible for this */
- public String agent() {
- return agent;
- }
-
- /** Name of the event */
- public String name() {
- return name;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Event event = (Event) o;
- return at.equals(event.at) && agent.equals(event.agent) && name.equals(event.name);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(at, agent, name);
- }
- }
-
- public static Builder builder() {
- return new Builder();
- }
-
- public static Builder builder(Node node) {
- return new Builder(node);
- }
-
- /**
- * Builder for a {@link Node}.
- *
- * The appropriate builder method must be called for any field that does not have a default value.
- */
- public static class Builder {
-
- private HostName hostname;
-
- private String id = UUID.randomUUID().toString();
- private Optional<HostName> parentHostname = Optional.empty();
- private State state = State.active;
- private NodeType type = NodeType.host;
- private NodeResources resources = NodeResources.unspecified();
- private Optional<ApplicationId> owner = Optional.empty();
- private Version currentVersion = Version.emptyVersion;
- private Version wantedVersion = Version.emptyVersion;
- private Version currentOsVersion = Version.emptyVersion;
- private Version wantedOsVersion = Version.emptyVersion;
- private boolean deferOsUpgrade = false;
- private DockerImage currentDockerImage = DockerImage.EMPTY;
- private DockerImage wantedDockerImage = DockerImage.EMPTY;
- private Optional<Instant> currentFirmwareCheck = Optional.empty();
- private Optional<Instant> wantedFirmwareCheck = Optional.empty();
- private ServiceState serviceState = ServiceState.expectedUp;
- private Optional<Instant> suspendedSince = Optional.empty();
- private long restartGeneration = 0;
- private long wantedRestartGeneration = 0;
- private long rebootGeneration = 0;
- private long wantedRebootGeneration = 0;
- private int cost = 0;
- private int failCount = 0;
- private Optional<String> flavor = Optional.empty();
- private String clusterId = "";
- private ClusterType clusterType = ClusterType.unknown;
- private String group = "";
- private int index = 0;
- private boolean retired = false;
- private boolean wantToRetire = false;
- private boolean wantToDeprovision = false;
- private boolean wantToRebuild = false;
- private boolean down = false;
- private Optional<TenantName> reservedTo = Optional.empty();
- private Optional<ApplicationId> exclusiveTo = Optional.empty();
- private Optional<ClusterType> exclusiveToClusterType = Optional.empty();
- private Map<String, String> reports = Map.of();
- private List<Event> history = List.of();
- private List<String> ipAddresses = List.of();
- private List<String> additionalIpAddresses = List.of();
- private List<String> additionalHostnames = List.of();
- private Optional<String> switchHostname = Optional.empty();
- private Optional<String> modelName = Optional.empty();
- private Environment environment = Environment.unknown;
- private CloudAccount cloudAccount = CloudAccount.empty;
-
- private Builder() {}
-
- private Builder(Node node) {
- this.id = node.id;
- this.hostname = node.hostname;
- this.parentHostname = node.parentHostname;
- this.state = node.state;
- this.type = node.type;
- this.resources = node.resources;
- this.owner = node.owner;
- this.currentVersion = node.currentVersion;
- this.wantedVersion = node.wantedVersion;
- this.currentOsVersion = node.currentOsVersion;
- this.wantedOsVersion = node.wantedOsVersion;
- this.deferOsUpgrade = node.deferOsUpgrade;
- this.currentDockerImage = node.currentDockerImage;
- this.wantedDockerImage = node.wantedDockerImage;
- this.serviceState = node.serviceState;
- this.suspendedSince = node.suspendedSince;
- this.currentFirmwareCheck = node.currentFirmwareCheck;
- this.wantedFirmwareCheck = node.wantedFirmwareCheck;
- this.restartGeneration = node.restartGeneration;
- this.wantedRestartGeneration = node.wantedRestartGeneration;
- this.rebootGeneration = node.rebootGeneration;
- this.wantedRebootGeneration = node.wantedRebootGeneration;
- this.cost = node.cost;
- this.failCount = node.failCount;
- this.flavor = node.flavor;
- this.clusterId = node.clusterId;
- this.clusterType = node.clusterType;
- this.group = node.group;
- this.index = node.index;
- this.retired = node.retired;
- this.wantToRetire = node.wantToRetire;
- this.wantToDeprovision = node.wantToDeprovision;
- this.wantToRebuild = node.wantToRebuild;
- this.down = node.down;
- this.reservedTo = node.reservedTo;
- this.exclusiveTo = node.exclusiveTo;
- this.exclusiveToClusterType = node.exclusiveToClusterType;
- this.reports = node.reports;
- this.history = node.history;
- this.ipAddresses = node.ipAddresses;
- this.additionalIpAddresses = node.additionalIpAddresses;
- this.additionalHostnames = node.additionalHostnames;
- this.switchHostname = node.switchHostname;
- this.modelName = node.modelName;
- this.environment = node.environment;
- }
-
- public Builder id(String id) {
- this.id = id;
- return this;
- }
-
- public Builder hostname(String hostname) {
- return hostname(HostName.of(hostname));
- }
-
- public Builder hostname(HostName hostname) {
- this.hostname = hostname;
- return this;
- }
-
- public Builder parentHostname(String parentHostname) {
- return parentHostname(HostName.of(parentHostname));
- }
-
- public Builder parentHostname(HostName parentHostname) {
- this.parentHostname = Optional.ofNullable(parentHostname);
- return this;
- }
-
- public Builder state(State state) {
- this.state = state;
- return this;
- }
-
- public Builder type(NodeType type) {
- this.type = type;
- return this;
- }
-
- public Builder resources(NodeResources resources) {
- this.resources = resources;
- return this;
- }
-
- public Builder owner(ApplicationId owner) {
- this.owner = Optional.ofNullable(owner);
- return this;
- }
-
- public Builder currentVersion(Version currentVersion) {
- this.currentVersion = currentVersion;
- return this;
- }
-
- public Builder wantedVersion(Version wantedVersion) {
- this.wantedVersion = wantedVersion;
- return this;
- }
-
- public Builder currentOsVersion(Version currentOsVersion) {
- this.currentOsVersion = currentOsVersion;
- return this;
- }
-
- public Builder wantedOsVersion(Version wantedOsVersion) {
- this.wantedOsVersion = wantedOsVersion;
- return this;
- }
-
- public Builder deferOsUpgrade(boolean deferOsUpgrade) {
- this.deferOsUpgrade = deferOsUpgrade;
- return this;
- }
-
- public Builder currentDockerImage(DockerImage currentDockerImage) {
- this.currentDockerImage = currentDockerImage;
- return this;
- }
-
- public Builder wantedDockerImage(DockerImage wantedDockerImage) {
- this.wantedDockerImage = wantedDockerImage;
- return this;
- }
-
- public Builder currentFirmwareCheck(Instant currentFirmwareCheck) {
- this.currentFirmwareCheck = Optional.ofNullable(currentFirmwareCheck);
- return this;
- }
-
- public Builder wantedFirmwareCheck(Instant wantedFirmwareCheck) {
- this.wantedFirmwareCheck = Optional.ofNullable(wantedFirmwareCheck);
- return this;
- }
-
- public Builder serviceState(ServiceState serviceState) {
- this.serviceState = serviceState;
- return this;
- }
-
- public Builder suspendedSince(Instant suspendedSince) {
- this.suspendedSince = Optional.ofNullable(suspendedSince);
- return this;
- }
-
- public Builder restartGeneration(long restartGeneration) {
- this.restartGeneration = restartGeneration;
- return this;
- }
-
- public Builder wantedRestartGeneration(long wantedRestartGeneration) {
- this.wantedRestartGeneration = wantedRestartGeneration;
- return this;
- }
-
- public Builder rebootGeneration(long rebootGeneration) {
- this.rebootGeneration = rebootGeneration;
- return this;
- }
-
- public Builder wantedRebootGeneration(long wantedRebootGeneration) {
- this.wantedRebootGeneration = wantedRebootGeneration;
- return this;
- }
-
- public Builder cost(int cost) {
- this.cost = cost;
- return this;
- }
-
- public Builder failCount(int failCount) {
- this.failCount = failCount;
- return this;
- }
-
- public Builder flavor(String flavor) {
- this.flavor = Optional.of(flavor);
- return this;
- }
-
- public Builder clusterId(String clusterId) {
- this.clusterId = clusterId;
- return this;
- }
-
- public Builder clusterType(ClusterType clusterType) {
- this.clusterType = clusterType;
- return this;
- }
-
- public Builder group(String group) {
- this.group = group;
- return this;
- }
-
- public Builder index(int index) {
- this.index = index;
- return this;
- }
-
- public Builder retired(boolean retired) {
- this.retired = retired;
- return this;
- }
-
- public Builder wantToRetire(boolean wantToRetire) {
- this.wantToRetire = wantToRetire;
- return this;
- }
-
- public Builder wantToDeprovision(boolean wantToDeprovision) {
- this.wantToDeprovision = wantToDeprovision;
- return this;
- }
-
- public Builder wantToRebuild(boolean wantToRebuild) {
- this.wantToRebuild = wantToRebuild;
- return this;
- }
-
- public Builder down(boolean down) {
- this.down = down;
- return this;
- }
-
- public Builder reservedTo(TenantName tenant) {
- this.reservedTo = Optional.of(tenant);
- return this;
- }
-
- public Builder exclusiveTo(ApplicationId exclusiveTo) {
- this.exclusiveTo = Optional.of(exclusiveTo);
- return this;
- }
-
- public Builder exclusiveToClusterType(ClusterType exclusiveToClusterType) {
- this.exclusiveToClusterType = Optional.of(exclusiveToClusterType);
- return this;
- }
-
- public Builder history(List<Event> history) {
- this.history = history;
- return this;
- }
-
- public Builder ipAddresses(List<String> ipAdresses) {
- this.ipAddresses = ipAdresses;
- return this;
- }
-
- public Builder additionalIpAddresses(List<String> additionalIpAddresses) {
- this.additionalIpAddresses = additionalIpAddresses;
- return this;
- }
-
- public Builder additionalHostnames(List<String> additionalHostnames) {
- this.additionalHostnames = additionalHostnames;
- return this;
- }
-
- public Builder switchHostname(String switchHostname) {
- this.switchHostname = Optional.ofNullable(switchHostname);
- return this;
- }
-
- public Builder modelName(String modelName) {
- this.modelName = Optional.ofNullable(modelName);
- return this;
- }
-
- public Builder reports(Map<String, String> reports) {
- this.reports = reports;
- return this;
- }
-
- public Builder environment(Environment environment) {
- this.environment = environment;
- return this;
- }
-
- public Builder cloudAccount(CloudAccount cloudAccount) {
- this.cloudAccount = cloudAccount;
- return this;
- }
-
- public Node build() {
- return new Node(id, hostname, parentHostname, state, type, resources, owner, currentVersion, wantedVersion,
- currentOsVersion, wantedOsVersion, deferOsUpgrade, currentFirmwareCheck, wantedFirmwareCheck, serviceState,
- suspendedSince, restartGeneration, wantedRestartGeneration, rebootGeneration,
- wantedRebootGeneration, cost, failCount, flavor, clusterId, clusterType, group, index, retired,
- wantToRetire, wantToDeprovision, wantToRebuild, down, reservedTo, exclusiveTo, wantedDockerImage,
- currentDockerImage, exclusiveToClusterType, reports, history, ipAddresses, additionalIpAddresses,
- additionalHostnames, switchHostname, modelName, environment, cloudAccount);
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeFilter.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeFilter.java
deleted file mode 100644
index 0870ecdbb6c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeFilter.java
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.google.common.collect.ImmutableSet;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.HostName;
-
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * A filter for listing nodes.
- *
- * This is immutable.
- *
- * @author mpolden
- */
-public class NodeFilter {
-
- private final boolean includeDeprovisioned;
- private final Set<Node.State> states;
- private final Set<HostName> hostnames;
- private final Set<HostName> parentHostnames;
- private final Set<ApplicationId> applications;
- private final Set<ClusterSpec.Id> clusterIds;
- private final Set<Node.ClusterType> clusterTypes;
-
- private NodeFilter(boolean includeDeprovisioned, Set<Node.State> states, Set<HostName> hostnames,
- Set<HostName> parentHostnames, Set<ApplicationId> applications,
- Set<ClusterSpec.Id> clusterIds, Set<Node.ClusterType> clusterTypes) {
- this.includeDeprovisioned = includeDeprovisioned;
- // Uses Guava Set to preserve insertion order
- this.states = ImmutableSet.copyOf(Objects.requireNonNull(states));
- this.hostnames = ImmutableSet.copyOf(Objects.requireNonNull(hostnames));
- this.parentHostnames = ImmutableSet.copyOf(Objects.requireNonNull(parentHostnames));
- this.applications = ImmutableSet.copyOf(Objects.requireNonNull(applications));
- this.clusterIds = ImmutableSet.copyOf(Objects.requireNonNull(clusterIds));
- this.clusterTypes = ImmutableSet.copyOf(Objects.requireNonNull(clusterTypes));
- if (!includeDeprovisioned && states.contains(Node.State.deprovisioned)) {
- throw new IllegalArgumentException("Must include deprovisioned nodes when matching deprovisioned state");
- }
- }
-
- public boolean includeDeprovisioned() {
- return includeDeprovisioned;
- }
-
- public Set<Node.State> states() {
- return states;
- }
-
- public Set<HostName> hostnames() {
- return hostnames;
- }
-
- public Set<HostName> parentHostnames() {
- return parentHostnames;
- }
-
- public Set<ApplicationId> applications() {
- return applications;
- }
-
- public Set<ClusterSpec.Id> clusterIds() {
- return clusterIds;
- }
-
- public Set<Node.ClusterType> clusterTypes() {
- return clusterTypes;
- }
-
- public NodeFilter includeDeprovisioned(boolean includeDeprovisioned) {
- return new NodeFilter(includeDeprovisioned, states, hostnames, parentHostnames, applications, clusterIds, clusterTypes);
- }
-
- public NodeFilter states(Node.State... states) {
- return states(ImmutableSet.copyOf(states));
- }
-
- public NodeFilter states(Set<Node.State> states) {
- return new NodeFilter(includeDeprovisioned, states, hostnames, parentHostnames, applications, clusterIds, clusterTypes);
- }
-
- public NodeFilter hostnames(HostName... hostnames) {
- return hostnames(ImmutableSet.copyOf(hostnames));
- }
-
- public NodeFilter hostnames(Set<HostName> hostnames) {
- return new NodeFilter(includeDeprovisioned, states, hostnames, parentHostnames, applications, clusterIds, clusterTypes);
- }
-
- public NodeFilter parentHostnames(HostName... parentHostnames) {
- return parentHostnames(ImmutableSet.copyOf(parentHostnames));
- }
-
- public NodeFilter parentHostnames(Set<HostName> parentHostnames) {
- return new NodeFilter(includeDeprovisioned, states, hostnames, parentHostnames, applications, clusterIds, clusterTypes);
- }
-
- public NodeFilter applications(ApplicationId... applications) {
- return applications(ImmutableSet.copyOf(applications));
- }
-
- public NodeFilter applications(Set<ApplicationId> applications) {
- return new NodeFilter(includeDeprovisioned, states, hostnames, parentHostnames, applications, clusterIds, clusterTypes);
- }
-
- public NodeFilter clusterIds(ClusterSpec.Id... clusterIds) {
- return clusterIds(ImmutableSet.copyOf(clusterIds));
- }
-
- public NodeFilter clusterIds(Set<ClusterSpec.Id> clusterIds) {
- return new NodeFilter(includeDeprovisioned, states, hostnames, parentHostnames, applications, clusterIds, clusterTypes);
- }
-
- public NodeFilter clusterTypes(Node.ClusterType... clusterTypes) {
- return clusterTypes(ImmutableSet.copyOf(clusterTypes));
- }
-
- public NodeFilter clusterTypes(Set<Node.ClusterType> clusterTypes) {
- return new NodeFilter(includeDeprovisioned, states, hostnames, parentHostnames, applications, clusterIds, clusterTypes);
- }
-
- /** A filter which matches all nodes, except deprovisioned ones */
- public static NodeFilter all() {
- return new NodeFilter(false, Set.of(), Set.of(), Set.of(), Set.of(), Set.of(), Set.of());
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepoStats.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepoStats.java
deleted file mode 100644
index 1082dbc4331..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepoStats.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import java.util.List;
-
-/**
- * @author bratseth
- */
-public class NodeRepoStats {
-
- private final double totalCost;
- private final double totalAllocatedCost;
- private final Load load;
- private final Load activeLoad;
- private final List<ApplicationStats> applicationStats;
-
- public NodeRepoStats(double totalCost, double totalAllocatedCost,
- Load load, Load activeLoad, List<ApplicationStats> applicationStats) {
- this.totalCost = totalCost;
- this.totalAllocatedCost = totalAllocatedCost;
- this.load = load;
- this.activeLoad = activeLoad;
- this.applicationStats = List.copyOf(applicationStats);
- }
-
- public double totalCost() { return totalCost; }
- public double totalAllocatedCost() { return totalAllocatedCost; }
- public Load load() { return load; }
- public Load activeLoad() { return activeLoad; }
- public List<ApplicationStats> applicationStats() { return applicationStats; }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepository.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepository.java
deleted file mode 100644
index 220acc4c615..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NodeRepository.java
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveUriUpdate;
-import com.yahoo.vespa.hosted.controller.api.integration.noderepository.ApplicationPatch;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * Node repository interface intended for use by the controller.
- *
- * @author mpolden
- */
-public interface NodeRepository {
-
- /** Add new nodes to the node repository */
- void addNodes(ZoneId zone, List<Node> nodes);
-
- /** Delete node */
- void deleteNode(ZoneId zone, String hostname);
-
- /** Move node to given state */
- void setState(ZoneId zone, Node.State state, String hostname);
-
- /** Get node from zone */
- Node getNode(ZoneId zone, String hostname);
-
- /** List nodes in given zone matching given filter */
- List<Node> list(ZoneId zone, NodeFilter filter);
-
- /** Get node repository's view of given application */
- Application getApplication(ZoneId zone, ApplicationId application);
-
- /** Update application */
- void patchApplication(ZoneId zone, ApplicationId application, ApplicationPatch patch);
-
- /** Get node statistics such as cost and load from given zone */
- NodeRepoStats getStats(ZoneId zone);
-
- /** Get all archive URIs found in zone */
- ArchiveUris getArchiveUris(ZoneId zone);
-
- /** Update some archive URI in the given zone */
- void updateArchiveUri(ZoneId zone, ArchiveUriUpdate archiveUriUpdate);
-
- /** Upgrade all nodes of given type to a new version */
- void upgrade(ZoneId zone, NodeType type, Version version, boolean allowDowngrade);
-
- /** Upgrade OS for all nodes of given type to a new version */
- void upgradeOs(ZoneId zone, NodeType type, Version version, boolean allowDowngrade);
-
- /** Get target versions for upgrades in given zone */
- TargetVersions targetVersionsOf(ZoneId zone);
-
- /** Requests firmware checks on all hosts in the given zone. */
- void requestFirmwareCheck(ZoneId zone);
-
- /** Cancels firmware checks on all hosts in the given zone. */
- void cancelFirmwareCheck(ZoneId zone);
-
- /** Retire given node */
- void retire(ZoneId zone, String hostname, boolean wantToRetire, boolean wantToDeprovision);
-
- /** Drop all documents on content nodes in the given zone, application and cluster */
- void dropDocuments(ZoneId zoneId, ApplicationId applicationId, Optional<ClusterSpec.Id> clusterId);
-
- /** Update reports for given node. A key with null value clears that report */
- void updateReports(ZoneId zone, String hostname, Map<String, String> reports);
-
- /** Update hardware model */
- void updateModel(ZoneId zone, String hostname, String modelName);
-
- /** Update switch hostname */
- void updateSwitchHostname(ZoneId zone, String hostname, String switchHostname);
-
- /** Schedule reboot of given node */
- void reboot(ZoneId zone, String hostname);
-
- /** Checks whether the zone has the spare capacity to remove the given hosts */
- boolean isReplaceable(ZoneId zone, List<HostName> hostnames);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NotFoundException.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NotFoundException.java
deleted file mode 100644
index 5d0a03f671b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/NotFoundException.java
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-/**
- * @author Tony Vaagenes
- */
-public class NotFoundException extends Exception {
- public NotFoundException(String msg) {
- super(msg);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/PrepareResponse.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/PrepareResponse.java
deleted file mode 100644
index 59d4762e365..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/PrepareResponse.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-
-import java.util.List;
-
-/**
- * @author Tony Vaagenes
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class PrepareResponse {
-
- public String message;
- public List<Log> log;
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ProxyResponse.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ProxyResponse.java
deleted file mode 100644
index 8e0f4d217f6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ProxyResponse.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.container.jdisc.HttpResponse;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Optional;
-
-/**
- * @author valerijf
- */
-public class ProxyResponse extends HttpResponse {
-
- private final byte[] content;
- private final String contentType;
-
- public ProxyResponse(byte[] content, String contentType, int status) {
- super(status);
- this.content = content;
- this.contentType = contentType;
- }
-
- @Override
- public void render(OutputStream outputStream) throws IOException {
- outputStream.write(content);
- }
-
- @Override
- public String getContentType() {
- return Optional.ofNullable(contentType).orElseGet(super::getContentType);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/QuotaUsage.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/QuotaUsage.java
deleted file mode 100644
index 3721a0cb85e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/QuotaUsage.java
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-/**
- * @author jonmv
- */
-public record QuotaUsage(double rate) {
-
- public static final QuotaUsage zero = new QuotaUsage(0);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/QuotaUsageResponse.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/QuotaUsageResponse.java
deleted file mode 100644
index 644f7bc8929..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/QuotaUsageResponse.java
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-
-/**
- * @author ogronnesby
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class QuotaUsageResponse {
- public double rate;
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ServiceConvergence.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ServiceConvergence.java
deleted file mode 100644
index 46e7502a612..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/ServiceConvergence.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.List;
-
-/**
- * Service convergence status for an application.
- *
- * @author mpolden
- * @author jonmv
- */
-public class ServiceConvergence {
-
- private final ApplicationId application;
- private final ZoneId zone;
- private final boolean converged;
- private final long wantedGeneration;
- private final List<Status> services;
-
- public ServiceConvergence(ApplicationId application, ZoneId zone, boolean converged,
- long wantedGeneration, List<Status> services) {
- this.application = application;
- this.zone = zone;
- this.converged = converged;
- this.wantedGeneration = wantedGeneration;
- this.services = List.copyOf(services);
- }
-
- public ApplicationId application() { return application; }
- public ZoneId zone() { return zone; }
- public boolean converged() { return converged; }
- public long wantedGeneration() { return wantedGeneration; }
- public List<Status> services() { return services; }
-
-
- /** Immutable class detailing the config status of a particular service for an application. */
- public static class Status {
- private final HostName host;
- private final long port;
- private final String type;
- private final long currentGeneration;
-
- public Status(HostName host, long port, String type, long currentGeneration) {
- this.host = host;
- this.port = port;
- this.type = type;
- this.currentGeneration = currentGeneration;
- }
-
- public HostName host() { return host; }
- public long port() { return port; }
- public String type() { return type; }
- public long currentGeneration() { return currentGeneration; }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/TargetVersions.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/TargetVersions.java
deleted file mode 100644
index d628542a22b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/TargetVersions.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.NodeType;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Target versions for node upgrades in a zone.
- *
- * @author mpolden
- */
-public class TargetVersions {
-
- public static final TargetVersions EMPTY = new TargetVersions(Map.of(), Map.of());
-
- private final Map<NodeType, Version> vespaVersions;
- private final Map<NodeType, Version> osVersions;
-
- public TargetVersions(Map<NodeType, Version> vespaVersions, Map<NodeType, Version> osVersions) {
- this.vespaVersions = Map.copyOf(Objects.requireNonNull(vespaVersions, "vespaVersions must be non-null"));
- this.osVersions = Map.copyOf(Objects.requireNonNull(osVersions, "osVersions must be non-null"));
- }
-
- /** Returns a copy of this with Vespa version set for given node type */
- public TargetVersions withVespaVersion(NodeType nodeType, Version version) {
- var vespaVersions = new HashMap<>(this.vespaVersions);
- vespaVersions.put(nodeType, version);
- return new TargetVersions(vespaVersions, osVersions);
- }
-
- /** Returns a copy of this with OS version set for given node type */
- public TargetVersions withOsVersion(NodeType nodeType, Version version) {
- var osVersions = new HashMap<>(this.osVersions);
- osVersions.put(nodeType, version);
- return new TargetVersions(vespaVersions, osVersions);
- }
-
- /** Returns the target OS version of given node type, if any */
- public Optional<Version> osVersion(NodeType type) {
- return Optional.ofNullable(osVersions.get(type));
- }
-
- /** Returns the target Vespa version of given node type, if any */
- public Optional<Version> vespaVersion(NodeType type) {
- return Optional.ofNullable(vespaVersions.get(type));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TargetVersions that = (TargetVersions) o;
- return vespaVersions.equals(that.vespaVersions) &&
- osVersions.equals(that.osVersions);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(vespaVersions, osVersions);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/package-info.java
deleted file mode 100644
index b9a689bcd67..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/configserver/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.configserver;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneToken.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneToken.java
deleted file mode 100644
index 6ebc9e407b6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneToken.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken;
-
-import java.time.Instant;
-import java.util.Optional;
-
-/**
- * Represents a generated data plane token.
- *
- * Note: This _MUST_ not be persisted.
- *
- * @author mortent
- */
-public record DataplaneToken(TokenId tokenId, FingerPrint fingerPrint, String tokenValue, Optional<Instant> expiration) {
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneTokenVersions.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneTokenVersions.java
deleted file mode 100644
index b209a385b6b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/DataplaneTokenVersions.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * List of dataplane token versions of a token id.
- *
- * @author mortent
- */
-public record DataplaneTokenVersions(TokenId tokenId, List<Version> tokenVersions, Instant lastUpdated) {
- public record Version(FingerPrint fingerPrint, String checkAccessHash, Instant creationTime,
- Optional<Instant> expiration, String author) {
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/FingerPrint.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/FingerPrint.java
deleted file mode 100644
index f5e8f8651f4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/FingerPrint.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken;
-
-import ai.vespa.validation.PatternedStringWrapper;
-
-import java.util.regex.Pattern;
-
-/**
- * A fingerprint to be used in dataplane token apis
- */
-public class FingerPrint extends PatternedStringWrapper<FingerPrint> {
-
- static final Pattern namePattern = Pattern.compile("([a-f0-9]{2}:)+[a-f0-9]{2}");
-
- private FingerPrint(String name) {
- super(name, namePattern, "fingerPrint");
- }
-
- public static FingerPrint of(String value) {
- return new FingerPrint(value);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/TokenId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/TokenId.java
deleted file mode 100644
index 7fefb5db2ba..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/TokenId.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken;
-
-import ai.vespa.validation.PatternedStringWrapper;
-
-import java.util.regex.Pattern;
-
-/**
- * A token id to be used in dataplane tokens
- */
-public class TokenId extends PatternedStringWrapper<TokenId> {
-
- static final Pattern namePattern = Pattern.compile("[A-Za-z][A-Za-z0-9_-]{0,59}");
-
- private TokenId(String name) {
- super(name, namePattern, "tokenId");
- }
-
- public static TokenId of(String value) {
- return new TokenId(value);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/package-info.java
deleted file mode 100644
index 95c5d40dbdc..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dataplanetoken/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationStore.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationStore.java
deleted file mode 100644
index 2c9078fe82b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationStore.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.time.Instant;
-import java.util.Optional;
-
-/**
- * Store for the application and test packages, diffs, and other metadata.
- *
- * @author smorgrav
- * @author jonmv
- */
-public interface ApplicationStore {
-
- /** Returns the application package of the given revision. */
- default byte[] get(DeploymentId deploymentId, RevisionId revisionId) {
- try (InputStream stream = stream(deploymentId, revisionId)) {
- return stream.readAllBytes();
- }
- catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- InputStream stream(DeploymentId deploymentId, RevisionId revisionId);
-
- /** Returns the application package diff, compared to the previous build, for the given tenant, application and build number */
- Optional<byte[]> getDiff(TenantName tenantName, ApplicationName applicationName, long buildNumber);
-
- /** Removes diffs for packages before the given build number */
- void pruneDiffs(TenantName tenantName, ApplicationName applicationName, long beforeBuildNumber);
-
- /** Find prod application package by given build number */
- Optional<byte[]> find(TenantName tenant, ApplicationName application, long buildNumber);
-
- /** Whether the prod application package with the given number is stored. */
- default boolean hasBuild(TenantName tenant, ApplicationName application, long buildNumber) {
- return find(tenant, application, buildNumber).isPresent();
- }
-
- /** Stores the given tenant application and test packages of the given revision, and diff since previous version. */
- void put(TenantName tenant, ApplicationName application, RevisionId revision, byte[] applicationPackage, byte[] testPackage, byte[] diff);
-
- /** Removes application and test packages older than the given revision, for the given application. */
- void prune(TenantName tenant, ApplicationName application, RevisionId revision);
-
- /** Removes all application and test packages for the given application, including any development package. */
- void removeAll(TenantName tenant, ApplicationName application);
-
- /** Returns the tester application package of the given revision. Does NOT contain the services.xml. */
- default byte[] getTester(TenantName tenant, ApplicationName application, RevisionId revision) {
- try (InputStream stream = streamTester(tenant, application, revision)) {
- return stream.readAllBytes();
- }
- catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- InputStream streamTester(TenantName tenantName, ApplicationName applicationName, RevisionId revision);
-
- /** Returns the application package diff, compared to the previous build, for the given deployment and build number */
- Optional<byte[]> getDevDiff(DeploymentId deploymentId, long buildNumber);
-
- /** Removes diffs for dev packages before the given build number */
- void pruneDevDiffs(DeploymentId deploymentId, long beforeBuildNumber);
-
- /** Stores the given application package as the development package for the given deployment and revision and diff since previous version. */
- void putDev(DeploymentId deploymentId, RevisionId revision, byte[] applicationPackage, byte[] diff);
-
- /** Stores the given application metadata with the current time as part of the path. */
- void putMeta(TenantName tenant, ApplicationName application, Instant now, byte[] metaZip);
-
- /** Marks the given application as deleted, and eligible for metadata GC at a later time. */
- void putMetaTombstone(TenantName tenant, ApplicationName application, Instant now);
-
- /** Stores the given manual deployment metadata with the current time as part of the path. */
- void putMeta(DeploymentId id, Instant now, byte[] metaZip);
-
- /** Marks the given manual deployment as deleted, and eligible for metadata GC at a later time. */
- void putMetaTombstone(DeploymentId id, Instant now);
-
- /** Prunes metadata such that only what was active at the given instant, and anything newer, is retained. */
- void pruneMeta(Instant oldest);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java
deleted file mode 100644
index 22b32d54bfd..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ApplicationVersion.java
+++ /dev/null
@@ -1,229 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.component.Version;
-
-import java.time.Instant;
-import java.util.Optional;
-import java.util.OptionalLong;
-
-import static ai.vespa.validation.Validation.requireAtLeast;
-import static java.util.Objects.requireNonNull;
-
-/**
- * An application package version, identified by a source revision and a build number.
- *
- * @author bratseth
- * @author mpolden
- * @author jonmv
- */
-public class ApplicationVersion implements Comparable<ApplicationVersion> {
-
- // This never changes and is only used to create a valid semantic version number, as required by application bundles
- private static final String majorVersion = "1.0";
-
- private final RevisionId id;
- private final Optional<SourceRevision> source;
- private final Optional<String> authorEmail;
- private final Optional<Version> compileVersion;
- private final Optional<Integer> allowedMajor;
- private final Optional<Instant> buildTime;
- private final Optional<String> sourceUrl;
- private final Optional<String> commit;
- private final Optional<String> bundleHash;
- private final Optional<Instant> obsoleteAt;
- private final boolean hasPackage;
- private final boolean shouldSkip;
- private final Optional<String> description;
- private final Optional<Instant> submittedAt;
- private final int risk;
-
- public ApplicationVersion(RevisionId id, Optional<SourceRevision> source, Optional<String> authorEmail,
- Optional<Version> compileVersion, Optional<Integer> allowedMajor, Optional<Instant> buildTime,
- Optional<String> sourceUrl, Optional<String> commit, Optional<String> bundleHash,
- Optional<Instant> obsoleteAt, boolean hasPackage, boolean shouldSkip, Optional<String> description,
- Optional<Instant> submittedAt, int risk) {
-
- if (commit.isPresent() && commit.get().length() > 128)
- throw new IllegalArgumentException("Commit may not be longer than 128 characters");
-
- if (authorEmail.isPresent() && ! authorEmail.get().matches("[^@]+@[^@]+"))
- throw new IllegalArgumentException("Invalid author email '" + authorEmail.get() + "'.");
-
- if (compileVersion.isPresent() && compileVersion.get().equals(Version.emptyVersion))
- throw new IllegalArgumentException("The empty version is not a legal compile version.");
-
- this.id = id;
- this.source = source;
- this.authorEmail = authorEmail;
- this.compileVersion = compileVersion;
- this.allowedMajor = requireNonNull(allowedMajor);
- this.buildTime = buildTime;
- this.sourceUrl = requireNonNull(sourceUrl, "sourceUrl cannot be null");
- this.commit = requireNonNull(commit, "commit cannot be null");
- this.bundleHash = bundleHash;
- this.obsoleteAt = obsoleteAt;
- this.hasPackage = hasPackage;
- this.shouldSkip = shouldSkip;
- this.description = description;
- this.submittedAt = requireNonNull(submittedAt);
- this.risk = requireAtLeast(risk, "application build risk", 0);
- }
-
- public RevisionId id() {
- return id;
- }
-
- /** Creates a minimal version for a development build. */
- public static ApplicationVersion forDevelopment(RevisionId id, Optional<Version> compileVersion, Optional<Integer> allowedMajor) {
- return new ApplicationVersion(id, Optional.empty(), Optional.empty(), compileVersion, allowedMajor, Optional.empty(),
- Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), true, false,
- Optional.empty(), Optional.empty(), 0);
- }
-
- /** Creates a version from a completed build, an author email, and build metadata. */
- public static ApplicationVersion forProduction(RevisionId id, Optional<SourceRevision> source, Optional<String> authorEmail,
- Optional<Version> compileVersion, Optional<Integer> allowedMajor, Optional<Instant> buildTime, Optional<String> sourceUrl,
- Optional<String> commit, Optional<String> bundleHash, Optional<String> description, Instant submittedAt, int risk) {
- return new ApplicationVersion(id, source, authorEmail, compileVersion, allowedMajor, buildTime,
- sourceUrl, commit, bundleHash, Optional.empty(), true, false,
- description, Optional.of(submittedAt), risk);
- }
-
- /** Returns a unique identifier for this version or "unknown" if version is not known */
- // TODO jonmv: kill
- public String stringId() {
- return source.map(SourceRevision::commit).map(ApplicationVersion::abbreviateCommit)
- .or(this::commit)
- .map(commit -> String.format("%s.%d-%s", majorVersion, buildNumber(), commit))
- .orElseGet(() -> majorVersion + "." + buildNumber());
- }
-
- /**
- * Returns information about the source of this revision, or empty if the source is not know/defined
- * (which is the case for command-line deployment from developers, but never for deployment jobs)
- */
- public Optional<SourceRevision> source() { return source; }
-
- /** Returns the build number of this version */
- public long buildNumber() { return id.number(); }
-
- /** Returns the email of the author of commit of this version, if known */
- public Optional<String> authorEmail() { return authorEmail; }
-
- /** Returns the Vespa version this package was compiled against, if known. */
- public Optional<Version> compileVersion() { return compileVersion; }
-
- public Optional<Integer> allowedMajor() { return allowedMajor; }
-
- /** Returns the time this package was built, if known. */
- public Optional<Instant> buildTime() { return buildTime; }
-
- /** Returns the hash of app package except deployment/build-meta data */
- public Optional<String> bundleHash() {
- return bundleHash;
- }
-
- /** Returns the source URL for this application version. */
- public Optional<String> sourceUrl() {
- return sourceUrl.or(() -> source.map(source -> {
- String repository = source.repository();
- if (repository.startsWith("git@"))
- repository = "https://" + repository.substring(4).replace(':', '/');
- if (repository.endsWith(".git"))
- repository = repository.substring(0, repository.length() - 4);
- return repository + "/tree/" + source.commit();
- }));
- }
-
- /** Returns the commit name of this application version. */
- public Optional<String> commit() { return commit.or(() -> source.map(SourceRevision::commit)); }
-
- /** Returns whether the application package for this version was deployed directly to zone */
- public boolean isDeployedDirectly() {
- return ! id.isProduction();
- }
-
- /** Returns a copy of this without a package stored. */
- public ApplicationVersion withoutPackage() {
- return new ApplicationVersion(id, source, authorEmail, compileVersion, allowedMajor, buildTime, sourceUrl, commit, bundleHash, obsoleteAt, false, shouldSkip, description, submittedAt, risk);
- }
-
- /** Returns a copy of this which is obsolete now. */
- public ApplicationVersion obsoleteAt(Instant now) {
- return new ApplicationVersion(id, source, authorEmail, compileVersion, allowedMajor, buildTime, sourceUrl, commit, bundleHash, Optional.of(now), hasPackage, shouldSkip, description, submittedAt, risk);
- }
-
- /** Returns the instant at which this became obsolete, i.e., no longer relevant for automated deployments. */
- public Optional<Instant> obsoleteAt() {
- return obsoleteAt;
- }
-
- /** Whether we still have the package for this revision. */
- public boolean hasPackage() {
- return hasPackage;
- }
-
- /** Returns a copy of this which will not be rolled out to production. */
- public ApplicationVersion skipped() {
- return new ApplicationVersion(id, source, authorEmail, compileVersion, allowedMajor, buildTime, sourceUrl, commit, bundleHash, obsoleteAt, hasPackage, true, description, submittedAt, risk);
- }
-
- /** Whether we have chosen to skip this version. */
- public boolean shouldSkip() {
- return shouldSkip;
- }
-
- /** Whether this revision can be deployed. */
- public boolean isDeployable() {
- return hasPackage && ! shouldSkip;
- }
-
- /** An optional, free-text description on this build. */
- public Optional<String> description() {
- return description;
- }
-
- /** Instant at which this version was submitted to the build system. */
- public Optional<Instant> submittedAt() {
- return submittedAt;
- }
-
- /** The assumed risk of rolling out this revision, relative to the previous. */
- public int risk() {
- return risk;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if ( ! (o instanceof ApplicationVersion)) return false;
- ApplicationVersion that = (ApplicationVersion) o;
- return id.equals(that.id);
- }
-
- @Override
- public int hashCode() {
- return id.hashCode();
- }
-
- @Override
- public String toString() {
- return id +
- source.map(s -> ", " + s).orElse("") +
- authorEmail.map(e -> ", by " + e).orElse("") +
- compileVersion.map(v -> ", built against " + v).orElse("") +
- buildTime.map(t -> " at " + t).orElse("") ;
- }
-
- /** Abbreviate given commit hash to 9 characters */
- private static String abbreviateCommit(String hash) {
- return hash.length() <= 9 ? hash : hash.substring(0, 9);
- }
-
- @Override
- public int compareTo(ApplicationVersion o) {
- return id.compareTo(o.id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ArtifactRepository.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ArtifactRepository.java
deleted file mode 100644
index 65e6f75a679..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/ArtifactRepository.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-
-/**
- * A repository of application build artifacts.
- *
- * @author mpolden
- */
-public interface ArtifactRepository {
-
- /** Returns the system application package of the given version. */
- byte[] getSystemApplicationPackage(ApplicationId application, ZoneId zone, Version version);
-
- /** Returns the current OS release with the given major version and tag */
- OsRelease osRelease(int major, OsRelease.Tag tag);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobId.java
deleted file mode 100644
index 8e809fab566..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobId.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.config.provision.ApplicationId;
-
-import java.util.Objects;
-
-/**
- * Immutable ID of a job that may be run.
- *
- * @author jonmv
- */
-public class JobId {
-
- private final ApplicationId application;
- private final JobType type;
-
- public JobId(ApplicationId application, JobType type) {
- this.application = Objects.requireNonNull(application, "ApplicationId cannot be null!");
- this.type = Objects.requireNonNull(type, "JobType cannot be null!");
- }
-
- public ApplicationId application() { return application; }
- public JobType type() { return type; }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- JobId jobId = (JobId) o;
- return application.equals(jobId.application) &&
- type.equals(jobId.type);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(application, type);
- }
-
- @Override
- public String toString() {
- return type.jobName() + " for " + application;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java
deleted file mode 100644
index fd9e222bb0c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobType.java
+++ /dev/null
@@ -1,234 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.config.provision.zone.ZoneList;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-
-import java.util.List;
-import java.util.stream.Stream;
-
-import static ai.vespa.validation.Validation.require;
-import static com.yahoo.config.provision.Environment.dev;
-import static com.yahoo.config.provision.Environment.perf;
-import static com.yahoo.config.provision.Environment.prod;
-import static com.yahoo.config.provision.Environment.staging;
-import static com.yahoo.config.provision.Environment.test;
-import static java.util.Comparator.naturalOrder;
-
-/**
- * Specification for a deployment and/or test job to run: what zone, and whether it is a production test.
- *
- * @author jonmv
- */
-public final class JobType implements Comparable<JobType> {
-
- private static final RegionName unknown = RegionName.from("unknown");
-
- private final String jobName;
- private final ZoneId zone;
- private final boolean isProductionTest;
-
- private JobType(String jobName, ZoneId zone, boolean isProductionTest) {
- this.jobName = jobName;
- this.zone = zone;
- this.isProductionTest = isProductionTest;
- }
-
- /** A system test in a test zone, or throws if no test zones are present.. */
- public static JobType systemTest(ZoneRegistry zones, CloudName cloud) {
- return testIn(test, zones, cloud);
- }
-
- /** A staging test in a staging zone, or throws if no staging zones are present. */
- public static JobType stagingTest(ZoneRegistry zones, CloudName cloud){
- return testIn(staging, zones, cloud);
- }
-
- /** Returns a test job in the given environment, preferring the given cloud, is possible; using the system cloud otherwise. */
- private static JobType testIn(Environment environment, ZoneRegistry zones, CloudName cloud) {
- if (cloud == null)
- return deploymentTo(ZoneId.from(environment, unknown));
-
- ZoneList candidates = zones.zones().controllerUpgraded().in(environment);
- if (candidates.in(cloud).zones().isEmpty())
- cloud = zones.systemZone().getCloudName();
-
- return candidates.in(cloud).zones().stream().findFirst()
- .map(zone -> deploymentTo(zone.getId()))
- .orElseThrow(() -> new IllegalArgumentException("no zones in " + environment + " among " +
- zones.zones().controllerUpgraded().zones()));
- }
-
- /** A deployment to the given dev region. */
- public static JobType dev(RegionName region) {
- return deploymentTo(ZoneId.from(dev, region));
- }
-
- /** A deployment to the given dev region. */
- public static JobType dev(String region) {
- return deploymentTo(ZoneId.from("dev", region));
- }
-
- /** A deployment to the given perf region. */
- public static JobType perf(RegionName region) {
- return deploymentTo(ZoneId.from(perf, region));
- }
-
- /** A deployment to the given perf region. */
- public static JobType perf(String region) {
- return deploymentTo(ZoneId.from("perf", region));
- }
-
- /** A deployment to the given prod region. */
- public static JobType prod(RegionName region) {
- return deploymentTo(ZoneId.from(prod, region));
- }
-
- /** A deployment to the given prod region. */
- public static JobType prod(String region) {
- return deploymentTo(ZoneId.from("prod", region));
- }
-
- /** A production test in the given region. */
- public static JobType test(RegionName region) {
- return productionTestOf(ZoneId.from(prod, region));
- }
-
- /** A production test in the given region. */
- public static JobType test(String region) {
- return productionTestOf(ZoneId.from("prod", region));
- }
-
- /** A deployment to the given zone; this may be a zone in the {@code test} or {@code staging} environments. */
- public static JobType deploymentTo(ZoneId zone) {
- String name;
- switch (zone.environment()) {
- case prod: name = "production-" + zone.region().value(); break;
- case test: name = "system-test"; break;
- case staging: name = "staging-test"; break;
- default: name = zone.environment().value() + "-" + zone.region().value();
- }
- return new JobType(name, zone, false);
- }
-
- /** A production test in the given production zone. */
- public static JobType productionTestOf(ZoneId zone) {
- String name = "test-" + require(zone.environment() == prod, zone, "must be prod zone").region().value();
- return new JobType(name, zone, true);
- }
-
- /** Creates a new job type from serialized zone data, and whether it is a production test; the inverse of {@link #serialized()} */
- public static JobType ofSerialized(String raw) {
- String[] parts = raw.split("\\.");
- if (parts.length == 2) return deploymentTo(ZoneId.from(parts[0], parts[1]));
- if (parts.length == 3 && "test".equals(parts[2])) return productionTestOf(ZoneId.from(parts[0], parts[1]));
- throw new IllegalArgumentException("illegal serialized job type '" + raw + "'");
- }
-
- /**
- * Creates a new job type from a job name, and a zone registry for looking up zones for the special system and staging test types.
- * Note: system and staging tests retrieved by job name always use the default cloud for the system!
- */
- public static JobType fromJobName(String jobName, ZoneRegistry zones) {
- switch (jobName) {
- case "system-test": return systemTest(zones, null);
- case "staging-test": return stagingTest(zones, null);
- }
- String[] parts = jobName.split("-", 2);
- if (parts.length == 2)
- switch (parts[0]) {
- case "production": return prod(parts[1]);
- case "test": return test(parts[1]);
- case "dev": return dev(parts[1]);
- case "perf": return perf(parts[1]);
- }
- throw new IllegalArgumentException("job name must be 'system-test', 'staging-test', or <production|test|dev|perf>-<region>, but got: " + jobName);
- }
-
- public static List<JobType> allIn(ZoneRegistry zones) {
- return zones.zones().reachable().zones().stream()
- .flatMap(zone -> zone.getEnvironment().isProduction() ? Stream.of(deploymentTo(zone.getId()), productionTestOf(zone.getId()))
- : zone.getEnvironment().isTest() ? Stream.of(deploymentTo(ZoneId.from(zone.getEnvironment(), unknown)))
- : Stream.of(deploymentTo(zone.getId())))
- .distinct()
- .sorted(naturalOrder())
- .toList();
- }
-
- /** A serialized form of this: {@code &lt;environment&gt;.&lt;region&gt;[.test]}; the inverse of {@link #ofSerialized(String)} */
- public String serialized() {
- return zone().value() + (isProductionTest ? ".test" : "");
- }
-
- public String jobName() {
- return jobName;
- }
-
- /** Returns the zone for this job. */
- public ZoneId zone() {
- // sigh ... but the alternative is worse.
- if (zone.region() == unknown)
- throw new IllegalStateException("this job type was not initiated with a proper zone, programming error");
-
- return zone;
- }
-
- public boolean isSystemTest() {
- return environment() == test;
- }
-
- public boolean isStagingTest() {
- return environment() == staging;
- }
-
- /** Returns whether this is a production job */
- public boolean isProduction() {
- return environment() == prod;
- }
-
- /** Returns whether this job runs tests */
- public boolean isTest() {
- return isProductionTest || environment().isTest();
- }
-
- /** Returns whether this job deploys to a zone */
- public boolean isDeployment() {
- return ! isProductionTest;
- }
-
- /** Returns the environment of this job type */
- public Environment environment() {
- return zone.environment();
- }
-
- @Override
- public int compareTo(JobType other) {
- int result;
- if (0 != (result = environment().compareTo(other.environment())) || environment().isTest()) return -result;
- if (0 != (result = zone.region().compareTo(other.zone.region()))) return result;
- return Boolean.compare(isProductionTest, other.isProductionTest);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- JobType jobType = (JobType) o;
- return jobName.equals(jobType.jobName);
- }
-
- @Override
- public int hashCode() {
- return jobName.hashCode();
- }
-
- @Override
- public String toString() {
- return jobName;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/OsRelease.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/OsRelease.java
deleted file mode 100644
index 465c01676e4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/OsRelease.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.component.Version;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Objects;
-
-/**
- * An OS release and its tag.
- *
- * @author mpolden
- */
-public record OsRelease(Version version, Tag tag, Instant taggedAt) {
-
- public OsRelease {
- Objects.requireNonNull(version);
- Objects.requireNonNull(tag);
- Objects.requireNonNull(taggedAt);
- }
-
- /** The version number */
- public Version version() {
- return version;
- }
-
- /** The tag of this */
- public Tag tag() {
- return tag;
- }
-
- /** Returns the time this was tagged */
- public Instant taggedAt() {
- return taggedAt;
- }
-
- /** Returns the age of this at given instant */
- public Duration age(Instant instant) {
- return Duration.between(taggedAt, instant);
- }
-
- @Override
- public String toString() {
- return "os release " + version + ", tagged " + tag + " at " + taggedAt;
- }
-
- /** Known release tags */
- public enum Tag {
- latest,
- stable,
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RevisionId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RevisionId.java
deleted file mode 100644
index b5d000ea5e9..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RevisionId.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import java.util.Objects;
-
-import static ai.vespa.validation.Validation.requireAtLeast;
-
-/**
- * ID of a revision of an application package. This is the build number, and whether it was submitted for production deployment.
- *
- * @author jonmv
- */
-public class RevisionId implements Comparable<RevisionId> {
-
- private final long number;
- private final JobId job;
-
- private RevisionId(long number, JobId job) {
- this.number = number;
- this.job = job;
- }
-
- public static RevisionId forProduction(long number) {
- return new RevisionId(requireAtLeast(number, "build number", 1L), null);
- }
-
- public static RevisionId forDevelopment(long number, JobId job) {
- return new RevisionId(requireAtLeast(number, "build number", 0L), job);
- }
-
- public long number() { return number; }
-
- public boolean isProduction() { return job == null; }
-
- /** Returns the job for this, if a development revision, or throws if this is a production revision. */
- public JobId job() { return Objects.requireNonNull(job, "production revisions have no associated job"); }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- RevisionId that = (RevisionId) o;
- return number == that.number && Objects.equals(job, that.job);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(number, job);
- }
-
- /** Unknown, manual builds sort first, then known manual builds, then production builds, by increasing build number */
- @Override
- public int compareTo(RevisionId o) {
- return isProduction() != o.isProduction() ? Boolean.compare(isProduction(), o.isProduction())
- : Long.compare(number, o.number);
- }
-
- @Override
- public String toString() {
- return isProduction() ? "build " + number
- : "dev build " + number + " for " + job.type().jobName() + " of " + job.application().instance();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RunId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RunId.java
deleted file mode 100644
index cb330768fb4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/RunId.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.config.provision.ApplicationId;
-
-import java.util.Objects;
-
-/**
- * Immutable ID of a deployment job.
- *
- * @author jonmv
- */
-public class RunId {
-
- private final JobId jobId;
- private final TesterId tester;
- private final long number;
-
- public RunId(ApplicationId application, JobType type, long number) {
- this.jobId = new JobId(application, type);
- this.tester = TesterId.of(application);
- if (number <= 0) throw new IllegalArgumentException("Build number must be a positive integer!");
- this.number = number;
- }
-
- public JobId job() { return jobId; }
- public ApplicationId application() { return jobId.application(); }
- public TesterId tester() { return tester; }
- public JobType type() { return jobId.type(); }
- public long number() { return number; }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- RunId runId = (RunId) o;
- return number == runId.number &&
- jobId.equals(runId.jobId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(jobId, number);
- }
-
- @Override
- public String toString() {
- return "run " + number + " of " + type().jobName() + " for " + application();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/SourceRevision.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/SourceRevision.java
deleted file mode 100644
index 1f7be73a68e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/SourceRevision.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import java.util.Objects;
-
-/**
- * A revision in a source repository
- *
- * @author bratseth
- */
-public class SourceRevision {
-
- private final String repository;
- private final String branch;
- private final String commit;
-
- public SourceRevision(String repository, String branch, String commit) {
- Objects.requireNonNull(repository, "repository cannot be null");
- Objects.requireNonNull(branch, "branch cannot be null");
- Objects.requireNonNull(commit, "commit cannot be null");
- this.repository = repository;
- this.branch = branch;
- this.commit = commit;
- }
-
- public String repository() { return repository; }
- public String branch() { return branch; }
- public String commit() { return commit; }
-
- @Override
- public int hashCode() { return Objects.hash(repository, branch, commit); }
-
- @Override
- public boolean equals(Object o) {
- if (o == this) return true;
- if ( ! (o instanceof SourceRevision)) return false;
-
- SourceRevision other = (SourceRevision)o;
- return this.repository.equals(other.repository) &&
- this.branch.equals(other.branch) &&
- this.commit.equals(other.commit);
- }
-
- @Override
- public String toString() { return "source revision of repository '" + repository +
- "', branch '" + branch + "' with commit '" + commit + "'"; }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TestReport.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TestReport.java
deleted file mode 100644
index b3ae9362ee1..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TestReport.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-/**
- * @author mortent
- */
-public class TestReport {
-
- private final String report;
-
- private TestReport(String report) {
- this.report = report;
- }
-
- public String toJson() {
- return report;
- }
-
- public static TestReport fromJson(String report) {
- return new TestReport(report);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TesterCloud.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TesterCloud.java
deleted file mode 100644
index 66cfb6edfe3..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TesterCloud.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.config.provision.EndpointsChecker.Endpoint;
-import com.yahoo.config.provision.EndpointsChecker.Availability;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-
-import java.net.URI;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Allows running some predefined tests -- typically remotely.
- *
- * @author jonmv
- */
-public interface TesterCloud {
-
- /** Signals the tester to run its tests. */
- void startTests(DeploymentId deploymentId, Suite suite, byte[] config);
-
- /** Returns the log entries from the tester with ids after the given threshold. */
- List<LogEntry> getLog(DeploymentId deploymentId, long after);
-
- /** Returns the current status of the tester. */
- Status getStatus(DeploymentId deploymentId);
-
- /** Returns whether the test container is ready to serve */
- boolean testerReady(DeploymentId deploymentId);
-
- Availability verifyEndpoints(DeploymentId deploymentId, List<Endpoint> endpoints, boolean initialDeployment);
-
- /** Returns the test report as JSON if available */
- Optional<TestReport> getTestReport(DeploymentId deploymentId);
-
- enum Status {
-
- /** Tests have not yet started. */
- NOT_STARTED,
-
- /** Tests are running. */
- RUNNING,
-
- /** Tests failed. */
- FAILURE,
-
- /** Tests were inconclusive, and need to run again later. */
- INCONCLUSIVE,
-
- /** The tester encountered an exception. */
- ERROR,
-
- /** No tests were found. */
- NO_TESTS,
-
- /** The tests were successful. */
- SUCCESS
-
- }
-
-
- enum Suite {
-
- system,
-
- staging_setup,
-
- staging,
-
- production;
-
- public static Suite of(JobType type, boolean isSetup) {
- if (type.isSystemTest()) return system;
- if (type.isStagingTest()) return isSetup ? staging_setup : staging;
- if (type.isProduction()) return production;
- throw new AssertionError("Unknown JobType '" + type + "'!");
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TesterId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TesterId.java
deleted file mode 100644
index 67316c6fb7f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/TesterId.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.config.provision.ApplicationId;
-
-/**
- * Holds an application ID for a tester application.
- *
- * @author jonmv
- */
-public class TesterId {
-
- public static final String suffix = "-t";
-
- private final ApplicationId id;
-
- private TesterId(ApplicationId id) {
- this.id = id;
- }
-
- /** Creates a new TesterId for a tester of the given application. */
- public static TesterId of(ApplicationId id) {
- return new TesterId(ApplicationId.from(id.tenant().value(),
- id.application().value(),
- id.instance().value() + suffix));
- }
-
- /** Returns the id of this tester application. */
- public ApplicationId id() {
- return id;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/package-info.java
deleted file mode 100644
index 4ce8e90db89..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java
deleted file mode 100644
index 1fece6518c6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTarget.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import ai.vespa.http.DomainName;
-
-import java.util.Objects;
-
-/**
- * The target of a {@link Record.Type#ALIAS} record. Contains record fields unique to aliases.
- *
- * @author mpolden
- */
-public sealed abstract class AliasTarget permits LatencyAliasTarget, WeightedAliasTarget {
-
- private final DomainName name;
- private final String dnsZone;
- private final String id;
-
- public AliasTarget(DomainName name, String dnsZone, String id) {
- this.name = Objects.requireNonNull(name, "name must be non-null");
- this.dnsZone = Objects.requireNonNull(dnsZone, "dnsZone must be non-null");
- this.id = Objects.requireNonNull(id, "id must be non-null");
- }
-
- /** A unique identifier of this record within the ALIAS record group */
- public String id() {
- return id;
- }
-
- /** DNS name this points to */
- public final DomainName name() {
- return name;
- }
-
- /** The DNS zone this belongs to */
- public final String dnsZone() {
- return dnsZone;
- }
-
- /** Returns the fields in this encoded as record data */
- public abstract RecordData pack();
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- AliasTarget alias = (AliasTarget) o;
- return name.equals(alias.name) &&
- dnsZone.equals(alias.dnsZone) &&
- id.equals(alias.id);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name, dnsZone, id);
- }
-
- /** Unpack target from given record data */
- public static AliasTarget unpack(RecordData data) {
- String[] parts = data.asString().split("/");
- return switch (parts[0]) {
- case LatencyAliasTarget.TARGET_TYPE -> LatencyAliasTarget.unpack(data);
- case WeightedAliasTarget.TARGET_TYPE -> WeightedAliasTarget.unpack(data);
- default -> throw new IllegalArgumentException("Unknown alias type '" + parts[0] + "'");
- };
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java
deleted file mode 100644
index 44ed81b576e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTarget.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import java.util.Objects;
-
-/**
- * Same as {@link AliasTarget}, except for targets outside AWS (cannot be targeted with ALIAS record).
- *
- * @author freva
- */
-public sealed abstract class DirectTarget permits LatencyDirectTarget, WeightedDirectTarget {
-
- private final RecordData recordData;
- private final String id;
-
- protected DirectTarget(RecordData recordData, String id) {
- this.recordData = Objects.requireNonNull(recordData, "recordData must be non-null");
- this.id = Objects.requireNonNull(id, "id must be non-null");
- }
-
- /** A unique identifier of this record within the record group */
- public String id() {
- return id;
- }
-
- /** Data in this, e.g. IP address for records of type A */
- public RecordData recordData() {
- return recordData;
- }
-
- /** Returns the fields in this encoded as record data */
- public abstract RecordData pack();
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- DirectTarget that = (DirectTarget) o;
- return recordData.equals(that.recordData) && id.equals(that.id);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(recordData, id);
- }
-
- /** Unpack target from given record data */
- public static DirectTarget unpack(RecordData data) {
- String[] parts = data.asString().split("/");
- return switch (parts[0]) {
- case LatencyDirectTarget.TARGET_TYPE -> LatencyDirectTarget.unpack(data);
- case WeightedDirectTarget.TARGET_TYPE -> WeightedDirectTarget.unpack(data);
- default -> throw new IllegalArgumentException("Unknown alias type '" + parts[0] + "'");
- };
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java
deleted file mode 100644
index 4318ebeaf89..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyAliasTarget.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import ai.vespa.http.DomainName;
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.Objects;
-
-/**
- * An implementation of {@link AliasTarget} that uses latency-based routing.
- *
- * @author mpolden
- */
-public final class LatencyAliasTarget extends AliasTarget {
-
- static final String TARGET_TYPE = "latency";
-
- private final ZoneId zone;
-
- public LatencyAliasTarget(DomainName name, String dnsZone, ZoneId zone) {
- super(name, dnsZone, zone.value());
- this.zone = Objects.requireNonNull(zone);
- }
-
- /** The zone this record points to */
- public ZoneId zone() {
- return zone;
- }
-
- @Override
- public RecordData pack() {
- return RecordData.from(String.join("/", TARGET_TYPE, name().value(), dnsZone(), id()));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- if (!super.equals(o)) return false;
- LatencyAliasTarget that = (LatencyAliasTarget) o;
- return zone.equals(that.zone);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(super.hashCode(), zone);
- }
-
- @Override
- public String toString() {
- return "latency target for " + name() + " [id=" + id() + ",dnsZone=" + dnsZone() + "]";
- }
-
- /** Unpack latency alias from given record data */
- public static LatencyAliasTarget unpack(RecordData data) {
- var parts = data.asString().split("/");
- if (parts.length != 4) {
- throw new IllegalArgumentException("Expected data to be on format type/name/DNS-zone/zone-id, but got " +
- data.asString());
- }
- if (!TARGET_TYPE.equals(parts[0])) {
- throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'");
- }
- return new LatencyAliasTarget(DomainName.of(parts[1]), parts[2], ZoneId.from(parts[3]));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java
deleted file mode 100644
index e117b8feef0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/LatencyDirectTarget.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.Objects;
-
-/**
- * An implementation of {@link DirectTarget} that uses latency-based routing.
- *
- * @author freva
- */
-public final class LatencyDirectTarget extends DirectTarget {
-
- static final String TARGET_TYPE = "latency";
-
- private final ZoneId zone;
-
- public LatencyDirectTarget(RecordData recordData, ZoneId zone) {
- super(recordData, zone.value());
- this.zone = Objects.requireNonNull(zone);
- }
-
- /** The zone this record points to */
- public ZoneId zone() {
- return zone;
- }
-
- @Override
- public RecordData pack() {
- return RecordData.from(String.join("/", TARGET_TYPE, recordData().asString(), id()));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- if (!super.equals(o)) return false;
- LatencyDirectTarget that = (LatencyDirectTarget) o;
- return zone.equals(that.zone);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(super.hashCode(), zone);
- }
-
- @Override
- public String toString() {
- return "latency target for " + recordData() + " [id=" + id() + "]";
- }
-
- /** Unpack latency alias from given record data */
- public static LatencyDirectTarget unpack(RecordData data) {
- var parts = data.asString().split("/");
- if (parts.length != 3) {
- throw new IllegalArgumentException("Expected data to be on format target-type/record-data/zone-id, but got " +
- data.asString());
- }
- if (!TARGET_TYPE.equals(parts[0])) {
- throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'");
- }
- return new LatencyDirectTarget(RecordData.from(parts[1]), ZoneId.from(parts[2]));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java
deleted file mode 100644
index 001daeb3893..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MemoryNameService.java
+++ /dev/null
@@ -1,124 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.ConcurrentSkipListSet;
-
-/**
- * An in-memory name service for testing purposes.
- *
- * @author mpolden
- */
-public class MemoryNameService implements NameService {
-
- private final Set<Record> records = new ConcurrentSkipListSet<>();
-
- public Set<Record> records() {
- return Collections.unmodifiableSet(records);
- }
-
- public void add(Record record) {
- Optional<Record> conflict = records.stream().filter(r -> conflicts(r, record)).findFirst();
- if (conflict.isPresent()) {
- throw new AssertionError("'" + record + "' conflicts with existing record '" +
- conflict.get() + "'");
- }
- records.add(record);
- }
-
- @Override
- public Record createRecord(Record.Type type, RecordName name, RecordData canonicalName) {
- var record = new Record(type, name, canonicalName);
- add(record);
- return record;
- }
-
- @Override
- public List<Record> createAlias(RecordName name, Set<AliasTarget> targets) {
- var records = targets.stream()
- .sorted((a, b) -> Comparator.comparing(AliasTarget::name).compare(a, b))
- .map(d -> new Record(Record.Type.ALIAS, name, d.pack()))
- .toList();
- // Satisfy idempotency contract of interface
- for (var r1 : records) {
- this.records.removeIf(r2 -> conflicts(r1, r2));
- }
- this.records.addAll(records);
- return records;
- }
-
- @Override
- public List<Record> createDirect(RecordName name, Set<DirectTarget> targets) {
- var records = targets.stream()
- .sorted((a, b) -> Comparator.comparing((DirectTarget target) -> target.recordData().asString()).compare(a, b))
- .map(d -> new Record(Record.Type.DIRECT, name, d.pack()))
- .toList();
- // Satisfy idempotency contract of interface
- for (var r1 : records) {
- this.records.removeIf(r2 -> conflicts(r1, r2));
- }
- this.records.addAll(records);
- return records;
- }
-
- @Override
- public List<Record> createTxtRecords(RecordName name, List<RecordData> txtData) {
- var records = txtData.stream()
- .map(data -> new Record(Record.Type.TXT, name, data))
- .toList();
- records.forEach(this::add);
- return records;
- }
-
- @Override
- public List<Record> findRecords(Record.Type type, RecordName name) {
- return records.stream()
- .filter(record -> record.type() == type && record.name().equals(name))
- .toList();
- }
-
- @Override
- public void updateRecord(Record record, RecordData newData) {
- var records = findRecords(record.type(), record.name());
- if (records.isEmpty()) {
- throw new IllegalArgumentException("No record with data '" + newData.asString() + "' exists");
- }
- if (records.size() > 1) {
- throw new IllegalArgumentException("Cannot update multi-value record '" + record.name().asString() +
- "' with '" + newData.asString() + "'");
- }
- var existing = records.get(0);
- this.records.remove(existing);
- add(new Record(existing.type(), existing.name(), newData));
- }
-
- @Override
- public void removeRecords(List<Record> records) {
- records.forEach(this.records::remove);
- }
-
- /**
- * Returns whether record r1 and r2 are in conflict. This attempts to enforce the same constraints a
- * most real name services.
- */
- private static boolean conflicts(Record r1, Record r2) {
- if (!r1.name().equals(r2.name())) return false; // Distinct names never conflict
- if (r1.type() == Record.Type.ALIAS && r1.type() == r2.type()) {
- AliasTarget t1 = AliasTarget.unpack(r1.data());
- AliasTarget t2 = AliasTarget.unpack(r2.data());
- return t1.name().equals(t2.name()); // ALIAS records require distinct targets
- }
- if (r1.type() == Record.Type.DIRECT && r1.type() == r2.type()) {
- DirectTarget t1 = DirectTarget.unpack(r1.data());
- DirectTarget t2 = DirectTarget.unpack(r2.data());
- return t1.id().equals(t2.id()); // DIRECT records require distinct IDs
- }
- return true; // Anything else is considered a conflict
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MockVpcEndpointService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MockVpcEndpointService.java
deleted file mode 100644
index 4391de45b1e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/MockVpcEndpointService.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import ai.vespa.http.DomainName;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record.Type;
-
-import java.time.Clock;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * @author jonmv
- */
-public class MockVpcEndpointService implements VpcEndpointService {
-
- public final AtomicBoolean enabled = new AtomicBoolean();
- public final Map<RecordName, ChallengeState> outcomes = new ConcurrentHashMap<>();
-
- private final Clock clock;
- private final NameService nameService;
-
- public MockVpcEndpointService(Clock clock, NameService nameService) {
- this.clock = clock;
- this.nameService = nameService;
- }
-
- @Override
- public synchronized Optional<DnsChallenge> setPrivateDns(DomainName privateDnsName, ClusterId clusterId, Optional<CloudAccount> account, boolean isGenerated) {
- DnsChallenge challenge = new DnsChallenge(RecordName.from("challenge--" + privateDnsName.value()),
- RecordData.from(account.map(CloudAccount::value).orElse("system")),
- clusterId,
- "service-id",
- account,
- clock.instant(),
- ChallengeState.pending);
- return Optional.ofNullable(enabled.get() && nameService.findRecords(Type.TXT, challenge.name()).isEmpty() ? challenge : null);
- }
-
- @Override
- public synchronized ChallengeState process(DnsChallenge challenge) {
- if (outcomes.containsKey(challenge.name())) return outcomes.get(challenge.name());
- if (nameService.findRecords(Type.TXT, challenge.name()).isEmpty()) throw new RuntimeException("No TXT record found for " + challenge.name());
- return ChallengeState.done;
- }
-
- @Override
- public synchronized List<VpcEndpoint> getConnections(ClusterId cluster, Optional<CloudAccount> account) {
- return List.of(new VpcEndpoint("endpoint-1", "available", EndpointState.open));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java
deleted file mode 100644
index 4eafe67da8f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/NameService.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import java.util.List;
-import java.util.Set;
-
-/**
- * A managed DNS service.
- *
- * @author mpolden
- */
-public interface NameService {
-
- /**
- * Create a new record
- *
- * @param type The DNS type of record to make, only a small set of types are supported, check with the implementation
- * @param name Name of the record, e.g. a FQDN for records of type A
- * @param data Data of the record, e.g. IP address for records of type A
- * @return The created record
- */
- Record createRecord(Record.Type type, RecordName name, RecordData data);
-
- /**
- * Create a non-standard ALIAS record pointing to given targets. Implementations of this are expected to be
- * idempotent
- *
- * @param targets Targets that should be resolved by this name.
- * @return The created records. One per target.
- */
- List<Record> createAlias(RecordName name, Set<AliasTarget> targets);
-
- /**
- * Create a non-standard record pointing to given targets. Implementations of this are expected to be
- * idempotent
- *
- * @param targets Targets that should be resolved by this name.
- * @return The created records. One per target.
- */
- List<Record> createDirect(RecordName name, Set<DirectTarget> targets);
-
- /**
- * Create a new TXT record containing the provided data.
- * @param name Name of the created record
- * @param txtRecords TXT data values for the record, each consisting of one or more space-separated double-quoted
- * strings: "string1" "string2"
- * @return The created records
- */
- List<Record> createTxtRecords(RecordName name, List<RecordData> txtRecords);
-
- /** Find all records matching given type and name */
- List<Record> findRecords(Record.Type type, RecordName name);
-
- /** Update existing record */
- void updateRecord(Record record, RecordData newData);
-
- /** Remove given record(s) */
- void removeRecords(List<Record> record);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java
deleted file mode 100644
index 93b9c23d587..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/Record.java
+++ /dev/null
@@ -1,80 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import java.time.Duration;
-import java.util.Comparator;
-import java.util.Objects;
-
-/**
- * A basic representation of a DNS resource record, containing the record type, name and data.
- *
- * @author mpolden
- */
-public record Record(Type type,
- Duration ttl,
- RecordName name,
- RecordData data) implements Comparable<Record> {
-
- private static final Comparator<Record> comparator = Comparator.comparing(Record::type)
- .thenComparing(Record::name)
- .thenComparing(Record::data);
-
- public Record {
- Objects.requireNonNull(type, "type cannot be null");
- Objects.requireNonNull(ttl, "ttl cannot be null");
- Objects.requireNonNull(name, "name cannot be null");
- Objects.requireNonNull(data, "data cannot be null");
- }
-
- public Record(Type type, RecordName name, RecordData data) {
- this(type, Duration.ofMinutes(5), name, data);
- }
-
- /** DNS type of this */
- public Type type() {
- return type;
- }
-
- /** The TTL value of this */
- public Duration ttl() {
- return ttl;
- }
-
- /** Data in this, e.g. IP address for records of type A */
- public RecordData data() {
- return data;
- }
-
- /** Name of this, e.g. a FQDN for records of type A */
- public RecordName name() {
- return name;
- }
-
- public enum Type {
- A,
- AAAA,
- ALIAS,
- CNAME,
- DIRECT,
- MX,
- NS,
- PTR,
- SOA,
- SRV,
- TXT,
- SPF,
- NAPTR,
- CAA,
- }
-
- @Override
- public String toString() {
- return String.format("%s %s -> %s [TTL: %s]", type, name, data, ttl);
- }
-
- @Override
- public int compareTo(Record that) {
- return comparator.compare(this, that);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordData.java
deleted file mode 100644
index 191ff81b514..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordData.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import java.util.Objects;
-
-/**
- * Represents the data field of a DNS record (RDATA).
- *
- * E.g. this may be an IP address for A records, or a FQDN for CNAME records.
- *
- * @author mpolden
- */
-public record RecordData(String data) implements Comparable<RecordData> {
-
- public RecordData {
- Objects.requireNonNull(data, "data cannot be null");
- }
-
- public String asString() {
- return data;
- }
-
- @Override
- public String toString() {
- return data;
- }
-
- /** Create data containing the given data */
- public static RecordData from(String data) {
- return new RecordData(data);
- }
-
- /** Create a new record and append a trailing dot to given data, if missing */
- public static RecordData fqdn(String data) {
- return from(data.endsWith(".") ? data : data + ".");
- }
-
- @Override
- public int compareTo(RecordData that) {
- return this.data.compareTo(that.data);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordName.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordName.java
deleted file mode 100644
index 678f91ac3db..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/RecordName.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import java.util.Objects;
-
-/**
- * Represents the name field of a DNS record (NAME).
- *
- * @author mpolden
- */
-public record RecordName(String name) implements Comparable<RecordName> {
-
- public RecordName {
- Objects.requireNonNull(name, "name cannot be null");
- }
-
- public String asString() {
- return name;
- }
-
- /** Returns whether this is a fully qualified domain name (ends in trailing dot) */
- public boolean isFqdn() {
- return name.endsWith(".");
- }
-
- /** Returns this as a fully qualified domain name (ends in trailing dot) */
- public RecordName asFqdn() {
- return isFqdn() ? this : new RecordName(name + ".");
- }
-
- @Override
- public String toString() {
- return name;
- }
-
- @Override
- public int compareTo(RecordName that) {
- return this.name.compareTo(that.name);
- }
-
- public static RecordName from(String name) {
- return new RecordName(name);
- }
-
- public static RecordName fqdn(String name) {
- return from(name).asFqdn();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/VpcEndpointService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/VpcEndpointService.java
deleted file mode 100644
index be3163e5e09..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/VpcEndpointService.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import ai.vespa.http.DomainName;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * @author jonmv
- */
-public interface VpcEndpointService {
-
- /** Create a TXT record with this name and token, and then complete the challenge. */
- record DnsChallenge(RecordName name, RecordData data, ClusterId clusterId, String serviceId,
- Optional<CloudAccount> account, Instant createdAt, ChallengeState state) {
-
- public DnsChallenge {
- requireNonNull(name, "name must be non-null");
- requireNonNull(data, "data must be non-null");
- requireNonNull(clusterId, "clusterId must be non-null");
- requireNonNull(serviceId, "serviceId must be non-null");
- requireNonNull(account, "account must be non-null");
- requireNonNull(createdAt, "createdAt must be non-null");
- requireNonNull(state, "state must be non-null");
- }
-
- public DnsChallenge withState(ChallengeState state) {
- return new DnsChallenge(name, data, clusterId, serviceId, account, createdAt, state);
- }
-
- }
-
- enum ChallengeState { pending, ready, running, done }
-
- /** Sets the private DNS name for any VPC endpoint for the given cluster, potentially guarded by a challenge. */
- Optional<DnsChallenge> setPrivateDns(DomainName privateDnsName, ClusterId clusterId, Optional<CloudAccount> account, boolean isGenerated);
-
- /** Attempts to complete the challenge, and returns the updated challenge state. */
- ChallengeState process(DnsChallenge challenge);
-
- /** A connection made to an endpoint service. */
- record VpcEndpoint(String endpointId, String stateString, EndpointState stateValue) { }
-
- enum EndpointState { pending, open, failed, closed }
-
- /** Lists all endpoints connected to an endpoint service (owned by account) for the given cluster. */
- List<VpcEndpoint> getConnections(ClusterId cluster, Optional<CloudAccount> account);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java
deleted file mode 100644
index ecf6ba806b7..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedAliasTarget.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import ai.vespa.http.DomainName;
-
-import java.util.Objects;
-
-/**
- * An implementation of {@link AliasTarget} where is requests are answered based on the weight assigned to the
- * record, as a proportion of the total weight for all records having the same DNS name.
- * <p>
- * The portion of received traffic is calculated as follows: (record weight / sum of the weights of all records).
- *
- * @author mpolden
- */
-public final class WeightedAliasTarget extends AliasTarget {
-
- static final String TARGET_TYPE = "weighted";
-
- private final long weight;
-
- public WeightedAliasTarget(DomainName name, String dnsZone, String id, long weight) {
- super(name, dnsZone, id);
- this.weight = weight;
- if (weight < 0) throw new IllegalArgumentException("Weight cannot be negative");
- }
-
- /** The weight of this target */
- public long weight() {
- return weight;
- }
-
- @Override
- public RecordData pack() {
- return RecordData.from(String.join("/", TARGET_TYPE, name().value(), dnsZone(), id(), Long.toString(weight)));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- if (!super.equals(o)) return false;
- WeightedAliasTarget that = (WeightedAliasTarget) o;
- return weight == that.weight;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(super.hashCode(), weight);
- }
-
- @Override
- public String toString() {
- return "weighted target for " + name() + "[id=" + id() + ",dnsZone=" + dnsZone() + ",weight=" + weight + "]";
- }
-
- /** Unpack weighted alias from given record data */
- public static WeightedAliasTarget unpack(RecordData data) {
- var parts = data.asString().split("/");
- if (parts.length != 5) {
- throw new IllegalArgumentException("Expected data to be on format type/name/DNS-zone/zone-id/weight, " +
- "but got " + data.asString());
- }
- if (!TARGET_TYPE.equals(parts[0])) {
- throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'");
- }
- return new WeightedAliasTarget(DomainName.of(parts[1]), parts[2], parts[3], Long.parseLong(parts[4]));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java
deleted file mode 100644
index 2417cd1b86a..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/WeightedDirectTarget.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.Objects;
-
-/**
- * An implementation of {@link DirectTarget} where is requests are answered based on the weight assigned to the
- * record, as a proportion of the total weight for all records having the same DNS name.
- * <p>
- * The portion of received traffic is calculated as follows: (record weight / sum of the weights of all records).
- *
- * @author freva
- */
-public final class WeightedDirectTarget extends DirectTarget {
-
- static final String TARGET_TYPE = "weighted";
-
- private final long weight;
-
- public WeightedDirectTarget(RecordData recordData, ZoneId zone, long weight) {
- super(recordData, zone.value());
- this.weight = weight;
- if (weight < 0) throw new IllegalArgumentException("Weight cannot be negative");
- }
-
- /** The weight of this target */
- public long weight() {
- return weight;
- }
-
- @Override
- public RecordData pack() {
- return RecordData.from(String.join("/", TARGET_TYPE, recordData().asString(), id(), Long.toString(weight)));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- if (!super.equals(o)) return false;
- WeightedDirectTarget that = (WeightedDirectTarget) o;
- return weight == that.weight;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(super.hashCode(), weight);
- }
-
- @Override
- public String toString() {
- return "weighted target for " + recordData() + "[id=" + id() + ",weight=" + weight + "]";
- }
-
- /** Unpack weighted alias from given record data */
- public static WeightedDirectTarget unpack(RecordData data) {
- var parts = data.asString().split("/");
- if (parts.length != 4) {
- throw new IllegalArgumentException("Expected data to be on format target-type/record-data/zone-id/weight, " +
- "but got " + data.asString());
- }
- if (!TARGET_TYPE.equals(parts[0])) {
- throw new IllegalArgumentException("Unexpected type '" + parts[0] + "'");
- }
- return new WeightedDirectTarget(RecordData.from(parts[1]), ZoneId.from(parts[2]), Long.parseLong(parts[3]));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/package-info.java
deleted file mode 100644
index db400080353..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/dns/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/EntityService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/EntityService.java
deleted file mode 100644
index 3762a42e573..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/EntityService.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.entity;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * A service which provides access to business-specific entities.
- *
- * @author mpolden
- */
-public interface EntityService {
-
- /** List all properties known by the service */
- Map<PropertyId, Property> listProperties();
-
- /** List all nodes owned by this system's property */
- List<NodeEntity> listNodes();
-
- Optional<NodeEntity> findNode(String hostname);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/MemoryEntityService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/MemoryEntityService.java
deleted file mode 100644
index df782fac230..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/MemoryEntityService.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.entity;
-
-import com.google.common.collect.ImmutableMap;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * @author mpolden
- */
-public class MemoryEntityService implements EntityService {
-
- private final Map<String, NodeEntity> nodeEntities = new HashMap<>();
-
- @Override
- public Map<PropertyId, Property> listProperties() {
- return ImmutableMap.of(new PropertyId("1234"), new Property("foo"),
- new PropertyId("4321"), new Property("bar"));
- }
-
- @Override
- public List<NodeEntity> listNodes() {
- return List.copyOf(nodeEntities.values());
- }
-
- @Override
- public Optional<NodeEntity> findNode(String hostname) {
- return Optional.empty();
- }
-
- public MemoryEntityService addNodeEntity(NodeEntity nodeEntity) {
- nodeEntities.put(nodeEntity.hostname(), nodeEntity);
- return this;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/NodeEntity.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/NodeEntity.java
deleted file mode 100644
index 0b8aaf3ec2d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/NodeEntity.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.entity;
-
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Information about a node from a {@link EntityService}.
- *
- * @author mpolden
- */
-public class NodeEntity {
-
- private final String hostname;
- private final Optional<String> model;
- private final Optional<String> manufacturer;
- private final Optional<String> switchHostname;
-
- public NodeEntity(String hostname, String model, String manufacturer, String switchHostname) {
- this.hostname = Objects.requireNonNull(hostname);
- this.model = nonBlank(model);
- this.manufacturer = nonBlank(manufacturer);
- this.switchHostname = nonBlank(switchHostname);
- }
-
- public String hostname() {
- return hostname;
- }
-
- /** The model name of this node */
- public Optional<String> model() {
- return model;
- }
-
- /** The manufacturer of this node */
- public Optional<String> manufacturer() {
- return manufacturer;
- }
-
- /** The hostname of network switch this node is connected to */
- public Optional<String> switchHostname() {
- return switchHostname;
- }
-
- private static Optional<String> nonBlank(String s) {
- return Optional.ofNullable(s).filter(v -> !v.isBlank());
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/package-info.java
deleted file mode 100644
index f86d785a717..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/entity/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.entity;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/HorizonClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/HorizonClient.java
deleted file mode 100644
index 438397d6aa4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/HorizonClient.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.horizon;
-
-/**
- * @author olaa
- */
-public interface HorizonClient {
-
- HorizonResponse getMetrics(byte[] query);
-
- HorizonResponse getUser();
-
- HorizonResponse getDashboard(int dashboardId);
-
- HorizonResponse getTopFolders();
-
- HorizonResponse getMetaData(byte[] query);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/HorizonResponse.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/HorizonResponse.java
deleted file mode 100644
index aaadb5109c7..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/HorizonResponse.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.horizon;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * @author valerijf
- */
-public class HorizonResponse implements AutoCloseable {
-
- private final int code;
- private final InputStream inputStream;
-
- public HorizonResponse(int code, InputStream inputStream) {
- this.code = code;
- this.inputStream = inputStream;
- }
-
- public int code() {
- return code;
- }
-
- public InputStream inputStream() {
- return inputStream;
- }
-
- public static HorizonResponse empty() {
- return new HorizonResponse(200, InputStream.nullInputStream());
- }
-
- @Override
- public void close() throws IOException {
- inputStream.close();
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/MockHorizonClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/MockHorizonClient.java
deleted file mode 100644
index 610d51c97df..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/MockHorizonClient.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.horizon;
-
-/**
- * @author olaa
- */
-public class MockHorizonClient implements HorizonClient {
-
- @Override
- public HorizonResponse getMetrics(byte[] query) {
- return HorizonResponse.empty();
- }
-
- @Override
- public HorizonResponse getUser() {
- return HorizonResponse.empty();
- }
-
- @Override
- public HorizonResponse getDashboard(int dashboardId) {
- return HorizonResponse.empty();
- }
-
- @Override
- public HorizonResponse getTopFolders() {
- return HorizonResponse.empty();
- }
-
- @Override
- public HorizonResponse getMetaData(byte[] query) {
- return HorizonResponse.empty();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/package-info.java
deleted file mode 100644
index 095616bd182..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/horizon/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.horizon;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/Jira.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/Jira.java
deleted file mode 100644
index b37b5d104c1..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/Jira.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.jira;
-
-import java.io.InputStream;
-import java.util.List;
-
-/**
- * @author mortent
- */
-public interface Jira {
-
- List<JiraIssue> searchByProjectAndSummary(String project, String summary);
-
- JiraIssue createIssue(JiraCreateIssue issue);
-
- void commentIssue(JiraIssue issue, JiraComment comment);
-
- void addAttachment(JiraIssue issue, String filename, InputStream fileContent);
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraComment.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraComment.java
deleted file mode 100644
index 28a55b6598c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraComment.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.jira;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * @author mortent
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class JiraComment {
-
- public final String body;
-
- @JsonCreator
- public JiraComment(@JsonProperty("body") String body) {
- this.body = body;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraCreateIssue.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraCreateIssue.java
deleted file mode 100644
index 3fec2fd64e0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraCreateIssue.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.jira;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.List;
-
-/**
- * @author mortent
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class JiraCreateIssue {
-
- @JsonProperty("fields")
- public final JiraFields fields;
-
- public JiraCreateIssue(JiraFields fields) {
- this.fields = fields;
- }
-
- public static class JiraFields {
- @JsonProperty("summary")
- public final String summary;
-
- @JsonProperty("description")
- public final String description;
-
- @JsonProperty("project")
- public final JiraProject project;
-
- @JsonProperty("issuetype")
- public final JiraIssueType issueType;
-
- @JsonProperty("components")
- public final List<JiraComponent> components;
-
- public JiraFields(
- JiraProject project,
- String summary,
- String description,
- JiraIssueType issueType,
- List<JiraComponent> components) {
- this.project = project;
- this.summary = summary;
- this.description = description;
- this.issueType = issueType;
- this.components = components;
- }
-
-
- public static class JiraProject {
- public static final JiraProject VESPA = new JiraProject("VESPA");
-
- @JsonProperty("key")
- public final String key;
-
- public JiraProject(String key) {
- this.key = key;
- }
- }
-
- public static class JiraIssueType {
- public static final JiraIssueType DEFECT = new JiraIssueType("Defect");
-
- @JsonProperty("name")
- public final String name;
-
- public JiraIssueType(String name) {
- this.name = name;
- }
- }
-
- public static class JiraComponent {
- public static final JiraComponent COREDUMPS = new JiraComponent("CoreDumps");
-
- @JsonProperty("name")
- public final String name;
-
-
- public JiraComponent(String name) {
- this.name = name;
- }
- }
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraIssue.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraIssue.java
deleted file mode 100644
index 905541008d7..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraIssue.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.jira;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonFormat;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.time.Instant;
-import java.util.Date;
-
-/**
- * @author mpolden
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class JiraIssue {
- public final String key;
- private final Fields fields;
-
- @JsonCreator
- public JiraIssue(@JsonProperty("key") String key, @JsonProperty("fields") Fields fields) {
- this.key = key;
- this.fields = fields;
- }
-
- public Instant lastUpdated() {
- return fields.lastUpdated;
- }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- public static class Fields {
- final Instant lastUpdated;
-
- @JsonCreator
- public Fields(
- @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'hh:mm:ss.SSSZ", timezone = "UTC")
- @JsonProperty("updated") Date updated) {
- lastUpdated = updated.toInstant();
- }
-
- public Fields(Instant instant) {
- this.lastUpdated = instant;
- }
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraIssues.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraIssues.java
deleted file mode 100644
index a607eeff1e8..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/JiraIssues.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.jira;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.Collections;
-import java.util.List;
-
-/**
- * @author mortent
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class JiraIssues {
- public final List<JiraIssue> issues;
-
- @JsonCreator
- public JiraIssues(@JsonProperty("issues") List<JiraIssue> issues) {
- this.issues = issues == null ? Collections.emptyList() : issues;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/package-info.java
deleted file mode 100644
index 21737655235..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/jira/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.jira;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/ArtifactId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/ArtifactId.java
deleted file mode 100644
index 2436112a998..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/ArtifactId.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.maven;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Identifier for an artifact.
- *
- * @author jonmv
- */
-public class ArtifactId {
-
- private final String groupId;
- private final String artifactId;
-
- public ArtifactId(String groupId, String artifactId) {
- this.groupId = requireNonNull(groupId);
- this.artifactId = requireNonNull(artifactId);
- }
-
- /** Group ID of this. */
- public String groupId() { return groupId; }
-
- /** Artifact ID of this. */
- public String artifactId() { return artifactId; }
-
- @Override
- public String toString() { return groupId + "." + artifactId; }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/MavenRepository.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/MavenRepository.java
deleted file mode 100644
index 189783d72ff..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/MavenRepository.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.maven;
-
-/**
- * A Maven repository which keeps released artifacts.
- *
- * @author jonmv
- */
-public interface MavenRepository {
-
- /** Returns metadata about all releases of a specific artifact to this repository. */
- Metadata metadata();
-
- /** Returns the id of the artifact whose releases this tracks. */
- ArtifactId artifactId();
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/Metadata.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/Metadata.java
deleted file mode 100644
index 9f833306109..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/Metadata.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.maven;
-
-import com.yahoo.component.Version;
-import com.yahoo.text.XML;
-import org.w3c.dom.Element;
-
-import java.time.Instant;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.List;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Metadata about a released artifact.
- *
- * @author jonmv
- */
-public class Metadata {
-
- private final ArtifactId id;
- private final Instant lastUpdated;
- private final List<Version> versions;
-
- public Metadata(ArtifactId id, Instant lastUpdated, List<Version> versions) {
- this.id = requireNonNull(id);
- this.lastUpdated = requireNonNull(lastUpdated);
- this.versions = versions.stream().sorted().toList();
- }
-
- /** Creates a new Metadata object from the given XML document. */
- public static Metadata fromXml(String xml) {
- Element metadata = XML.getDocument(xml).getDocumentElement();
- ArtifactId id = new ArtifactId(XML.getValue(XML.getChild(metadata, "groupId")),
- XML.getValue(XML.getChild(metadata, "artifactId")));
- String lastUpdatedTimestamp = XML.getValue(XML.getChild(XML.getChild(metadata, "versioning"), "lastUpdated"));
- Instant lastUpdated = Instant.from(DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(ZoneId.of("UTC"))
- .parse(lastUpdatedTimestamp));
- List<Version> versions = new ArrayList<>();
- for (Element version : XML.getChildren(XML.getChild(XML.getChild(metadata, "versioning"), "versions")))
- versions.add(Version.fromString(XML.getValue(version)));
-
- return new Metadata(id, lastUpdated, versions);
- }
-
- /** Id of the metadata this concerns. */
- public ArtifactId id() { return id; }
-
- /** When the list of versions was last updated. */
- Instant lastUpdated() { return lastUpdated; }
-
- /** List of available versions of this, sorted by ascending version order. */
- public List<Version> versions(Instant availableAt) {
- return versions.size() == 1 || availableAt.isAfter(lastUpdated.plusSeconds(10800)) ? versions : versions.subList(0, versions.size() - 1);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/package-info.java
deleted file mode 100644
index 05e6d7e2d8b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/maven/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.maven;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationData.java
deleted file mode 100644
index a51bcc03295..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationData.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Application;
-
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/**
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class ApplicationData {
-
- @JsonProperty
- public String id;
- @JsonProperty
- public Map<String, ClusterData> clusters;
-
- public Application toApplication() {
- return new Application(ApplicationId.fromFullString(id),
- clusters.entrySet().stream().map(e -> e.getValue().toCluster(e.getKey())).toList());
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationPatch.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationPatch.java
deleted file mode 100644
index b0f2d451122..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationPatch.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Patchable data under Application
- *
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class ApplicationPatch {
-
- @JsonProperty("currentReadShare")
- public Double currentReadShare;
-
- @JsonProperty("maxReadShare")
- public Double maxReadShare;
-
- @JsonProperty("clusters")
- public Map<String, ClusterPatch> clusters = new LinkedHashMap<>();
-
- public static class ClusterPatch {
-
- @JsonProperty("bcpGroupInfo")
- public BcpGroupInfo bcpGroupInfo;
-
- public ClusterPatch(BcpGroupInfo bcpGroupInfo) {
- this.bcpGroupInfo = bcpGroupInfo;
- }
-
- }
-
- public static class BcpGroupInfo {
-
- @JsonProperty("queryRate")
- public Double queryRate;
-
- @JsonProperty("growthRateHeadroom")
- public Double growthRateHeadroom;
-
- @JsonProperty("cpuCostPerQuery")
- public Double cpuCostPerQuery;
-
- public BcpGroupInfo(@JsonProperty("queryRate")double queryRate,
- @JsonProperty("growthRateHeadroom")double growthRateHeadroom,
- @JsonProperty("cpuCostPerQuery")double cpuCostPerQuery) {
- this.queryRate = queryRate;
- this.growthRateHeadroom = growthRateHeadroom;
- this.cpuCostPerQuery = cpuCostPerQuery;
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationStatsData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationStatsData.java
deleted file mode 100644
index 84bf9bf29aa..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ApplicationStatsData.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationStats;
-
-/**
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class ApplicationStatsData {
-
- @JsonProperty("id")
- public String id;
-
- @JsonProperty("load")
- public LoadData load;
-
- @JsonProperty("cost")
- public Double cost;
-
- @JsonProperty("unutilizedCost")
- public Double unutilizedCost;
-
- public ApplicationStats toApplicationStats() {
- return new ApplicationStats(ApplicationId.fromFullString(id), load.toLoad(), cost, unutilizedCost);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ArchiveList.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ArchiveList.java
deleted file mode 100644
index f2eddbc9a5f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ArchiveList.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class ArchiveList {
- @JsonProperty("archives")
- public List<Archive> archives = new ArrayList<>();
-
- public static class Archive {
- @JsonProperty("tenant")
- public String tenant;
-
- @JsonProperty("account")
- public String account;
-
- @JsonProperty("uri")
- public String uri;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ArchivePatch.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ArchivePatch.java
deleted file mode 100644
index 0303d0580dc..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ArchivePatch.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * @author valerijf
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class ArchivePatch {
-
- @JsonProperty("uri")
- private final String uri;
-
- @JsonCreator
- public ArchivePatch(@JsonProperty("uri") String uri) {
- this.uri = uri;
- }
-
- @JsonInclude
- public String uri() {
- return uri;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/AutoscalingData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/AutoscalingData.java
deleted file mode 100644
index 80db6daba28..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/AutoscalingData.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Load;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Optional;
-
-/**
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class AutoscalingData {
-
- @JsonProperty("status")
- public String status;
-
- @JsonProperty("description")
- public String description;
-
- @JsonProperty("resources")
- public ClusterResourcesData resources;
-
- @JsonProperty("at")
- public Long at;
-
- @JsonProperty("peak")
- public LoadData peak;
-
- @JsonProperty("ideal")
- public LoadData ideal;
-
- @JsonProperty("metrics")
- public AutoscalingMetricsData metrics;
-
- public Cluster.Autoscaling toAutoscaling() {
- return new Cluster.Autoscaling(status == null ? "" : status,
- description == null ? "" : description,
- resources == null ? Optional.empty() : Optional.ofNullable(resources.toClusterResources()),
- at == null ? Instant.EPOCH : Instant.ofEpochMilli(at),
- peak == null ? Load.zero() : peak.toLoad(),
- ideal == null ? Load.zero() : ideal.toLoad(),
- metrics == null ? Cluster.Autoscaling.Metrics.zero() : metrics.toMetrics());
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/AutoscalingMetricsData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/AutoscalingMetricsData.java
deleted file mode 100644
index 303869397ab..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/AutoscalingMetricsData.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
-
-/**
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class AutoscalingMetricsData {
-
- @JsonProperty("queryRate")
- public Double queryRate;
-
- @JsonProperty("growthRateHeadroom")
- public Double growthRateHeadroom;
-
- @JsonProperty("cpuCostPerQuery")
- public Double cpuCostPerQuery;
-
- public Cluster.Autoscaling.Metrics toMetrics() {
- return new Cluster.Autoscaling.Metrics(queryRate, growthRateHeadroom, cpuCostPerQuery);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/Capacity.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/Capacity.java
deleted file mode 100644
index 3919697fd60..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/Capacity.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * @author olaa
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class Capacity {
-
- @JsonProperty("removalPossible")
- public boolean removalPossible;
-
- public boolean isRemovalPossible() {
- return removalPossible;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ClusterData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ClusterData.java
deleted file mode 100644
index a2e78a42b21..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ClusterData.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.IntRange;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-/**
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class ClusterData {
-
- @JsonProperty("type")
- public String type;
-
- @JsonProperty("min")
- public ClusterResourcesData min;
-
- @JsonProperty("max")
- public ClusterResourcesData max;
-
- @JsonProperty("groupSize")
- public IntRangeData groupSize;
-
- @JsonProperty("current")
- public ClusterResourcesData current;
-
- @JsonProperty("suggested")
- public AutoscalingData suggested;
-
- @JsonProperty("target")
- public AutoscalingData target;
-
- @JsonProperty("scalingEvents")
- public List<ScalingEventData> scalingEvents;
-
- @JsonProperty("scalingDuration")
- public Long scalingDuration;
-
- public Cluster toCluster(String id) {
- return new Cluster(ClusterSpec.Id.from(id),
- ClusterSpec.Type.from(type),
- min.toClusterResources(),
- max.toClusterResources(),
- groupSize == null ? IntRange.empty() : groupSize.toRange(),
- current.toClusterResources(),
- target == null ? Cluster.Autoscaling.empty() : target.toAutoscaling(),
- suggested == null ? Cluster.Autoscaling.empty() : suggested.toAutoscaling(),
- scalingEvents == null ? List.of()
- : scalingEvents.stream().map(data -> data.toScalingEvent()).toList(),
- scalingDuration == null ? Duration.ofMillis(0) : Duration.ofMillis(scalingDuration));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ClusterResourcesData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ClusterResourcesData.java
deleted file mode 100644
index ca9387d6e18..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ClusterResourcesData.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.config.provision.ClusterResources;
-
-import java.util.Optional;
-
-/**
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class ClusterResourcesData {
-
- @JsonProperty
- public int nodes;
- @JsonProperty
- public int groups;
- @JsonProperty
- public NodeResources resources;
-
- public ClusterResources toClusterResources() {
- if (resources == null) return null; // TODO: Compatibility, remove after January 2023
- return new ClusterResources(nodes, groups, resources.toNodeResources());
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/IntRangeData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/IntRangeData.java
deleted file mode 100644
index 52741a59158..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/IntRangeData.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.config.provision.IntRange;
-
-import java.util.OptionalInt;
-
-/**
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class IntRangeData {
-
- @JsonProperty("from")
- public Integer from;
-
- @JsonProperty("to")
- public Integer to;
-
- public IntRange toRange() {
- return new IntRange(from == null ? OptionalInt.empty() : OptionalInt.of(from),
- to == null ? OptionalInt.empty() : OptionalInt.of(to));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/LoadData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/LoadData.java
deleted file mode 100644
index 7cafe6f20d0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/LoadData.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Load;
-
-/**
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class LoadData {
-
- @JsonProperty("cpu")
- public Double cpu;
-
- @JsonProperty("memory")
- public Double memory;
-
- @JsonProperty("disk")
- public Double disk;
-
- public Load toLoad() {
- return new Load(cpu, memory, disk);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/MaintenanceJobList.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/MaintenanceJobList.java
deleted file mode 100644
index 895f7e54086..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/MaintenanceJobList.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-
-import java.util.ArrayList;
-import java.util.List;
-
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class MaintenanceJobList {
- @JsonProperty("jobs")
- public List<MaintenanceJobName> jobs = new ArrayList<>();
- @JsonProperty("inactive")
- public List<String> inactive = new ArrayList<>();
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/MaintenanceJobName.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/MaintenanceJobName.java
deleted file mode 100644
index 2acea8c506f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/MaintenanceJobName.java
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class MaintenanceJobName {
- @JsonProperty("name")
- public String name;
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeHistory.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeHistory.java
deleted file mode 100644
index 0ca4acdcf49..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeHistory.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * Wire class for node-repository representation of the history of a node
- *
- * @author smorgrav
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class NodeHistory {
-
- @JsonProperty("at")
- public Long at;
- @JsonProperty("agent")
- public String agent;
- @JsonProperty("event")
- public String event;
-
- public Long getAt() {
- return at;
- }
-
- public String getAgent() {
- return agent;
- }
-
- public String getEvent() {
- return event;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeList.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeList.java
deleted file mode 100644
index c44d9e4fd2f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeList.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.List;
-
-/**
- * @author mortent
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class NodeList {
- @JsonProperty
- List<NodeRepositoryNode> nodes;
-
- public NodeList() {
- }
-
- public NodeList(List<NodeRepositoryNode> nodes) {
- this.nodes = nodes;
- }
-
- public List<NodeRepositoryNode> nodes() {
- return nodes;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeMembership.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeMembership.java
deleted file mode 100644
index 3d3bbddd349..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeMembership.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * @author mpolden
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class NodeMembership {
-
- @JsonProperty
- public String clustertype;
- @JsonProperty
- public String clusterid;
- @JsonProperty
- public String group;
- @JsonProperty
- public Integer index;
- @JsonProperty
- public Boolean retired;
-
- public String getClustertype() {
- return clustertype;
- }
-
- public String getClusterid() {
- return clusterid;
- }
-
- public String getGroup() {
- return group;
- }
-
- public Integer getIndex() {
- return index;
- }
-
- public Boolean getRetired() {
- return retired;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeOwner.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeOwner.java
deleted file mode 100644
index 076d81aa5aa..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeOwner.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.Objects;
-
-/**
- * @author mpolden
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class NodeOwner {
-
- @JsonProperty
- public String tenant;
- @JsonProperty
- public String application;
- @JsonProperty
- public String instance;
-
- public NodeOwner() {}
-
- public String getTenant() {
- return tenant;
- }
-
- public String getApplication() {
- return application;
- }
-
- public String getInstance() {
- return instance;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- NodeOwner nodeOwner = (NodeOwner) o;
- return Objects.equals(tenant, nodeOwner.tenant) &&
- Objects.equals(application, nodeOwner.application) &&
- Objects.equals(instance, nodeOwner.instance);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(tenant, application, instance);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeRepoStatsData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeRepoStatsData.java
deleted file mode 100644
index 0c516a47e13..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeRepoStatsData.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepoStats;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class NodeRepoStatsData {
-
- @JsonProperty("totalCost")
- public Double totalCost;
-
- @JsonProperty("totalAllocatedCost")
- public Double totalAllocatedCost;
-
- @JsonProperty("load")
- public LoadData load;
-
- @JsonProperty("activeLoad")
- public LoadData activeLoad;
-
- @JsonProperty("applications")
- public List<ApplicationStatsData> applications;
-
- public NodeRepoStats toNodeRepoStats() {
- return new NodeRepoStats(totalCost, totalAllocatedCost,
- load.toLoad(), activeLoad.toLoad(),
- applications.stream().map(stats -> stats.toApplicationStats()).toList());
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeRepositoryNode.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeRepositoryNode.java
deleted file mode 100644
index 48ebaf61c18..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeRepositoryNode.java
+++ /dev/null
@@ -1,533 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonGetter;
-import com.fasterxml.jackson.annotation.JsonIgnore;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.JsonNode;
-
-import java.util.List;
-import java.util.Map;
-
-/**
- * The wire format of a node retrieved from the node repository.
- *
- * All fields in this are nullable.
- *
- * @author bjorncs
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class NodeRepositoryNode {
-
- @JsonProperty("url")
- private String url;
- @JsonProperty("id")
- private String id;
- @JsonProperty("state")
- private String state;
- @JsonProperty("hostname")
- private String hostname;
- @JsonProperty("ipAddresses")
- private List<String> ipAddresses;
- @JsonProperty("additionalIpAddresses")
- private List<String> additionalIpAddresses;
- @JsonProperty("additionalHostnames")
- private List<String> additionalHostnames;
- @JsonProperty("flavor")
- private String flavor;
- @JsonProperty("resources")
- private NodeResources resources;
- @JsonProperty("requestedResources")
- private NodeResources requestedResources;
- @JsonProperty("membership")
- private NodeMembership membership;
- @JsonProperty("owner")
- private NodeOwner owner;
- @JsonProperty("restartGeneration")
- private Integer restartGeneration;
- @JsonProperty("rebootGeneration")
- private Integer rebootGeneration;
- @JsonProperty("currentRestartGeneration")
- private Integer currentRestartGeneration;
- @JsonProperty("currentRebootGeneration")
- private Integer currentRebootGeneration;
- @JsonProperty("vespaVersion")
- private String vespaVersion;
- @JsonProperty("wantedVespaVersion")
- private String wantedVespaVersion;
- @JsonProperty("currentOsVersion")
- private String currentOsVersion;
- @JsonProperty("wantedOsVersion")
- private String wantedOsVersion;
- @JsonProperty("deferOsUpgrade")
- private Boolean deferOsUpgrade;
- @JsonProperty("currentFirmwareCheck")
- private Long currentFirmwareCheck;
- @JsonProperty("wantedFirmwareCheck")
- private Long wantedFirmwareCheck;
- @JsonProperty("failCount")
- private Integer failCount;
- @JsonProperty("environment")
- private String environment;
- @JsonProperty("type")
- private String type;
- @JsonProperty("wantedDockerImage")
- private String wantedDockerImage;
- @JsonProperty("currentDockerImage")
- private String currentDockerImage;
- @JsonProperty("parentHostname")
- private String parentHostname;
- @JsonProperty("wantToRetire")
- private Boolean wantToRetire;
- @JsonProperty("wantToDeprovision")
- private Boolean wantToDeprovision;
- @JsonProperty("wantToRebuild")
- private Boolean wantToRebuild;
- @JsonProperty("down")
- private Boolean down;
- @JsonProperty("cost")
- private Integer cost;
- @JsonProperty("history")
- private List<NodeHistory> history;
- @JsonProperty("orchestratorStatus")
- private String orchestratorStatus;
- @JsonProperty("suspendedSinceMillis")
- private Long suspendedSinceMillis;
- @JsonProperty("reports")
- private Map<String, JsonNode> reports;
- @JsonProperty("modelName")
- private String modelName;
- @JsonProperty("reservedTo")
- private String reservedTo;
- @JsonProperty("exclusiveTo")
- private String exclusiveTo;
- @JsonProperty("exclusiveToClusterType")
- private String exclusiveToClusterType;
- @JsonProperty("switchHostname")
- private String switchHostname;
- @JsonProperty("cloudAccount")
- private String cloudAccount;
- @JsonProperty("wireguardPubKey")
- private String wireguardPubKey;
- @JsonProperty("archiveUri")
- private String archiveUri;
-
- public String getUrl() {
- return url;
- }
-
- public void setUrl(String url) {
- this.url = url;
- }
-
- public String getId() {
- return id;
- }
-
- public void setId(String id) {
- this.id = id;
- }
-
- public String getState() {
- return state;
- }
-
- public void setState(String state) {
- this.state = state;
- }
-
- public String getHostname() {
- return hostname;
- }
-
- public void setHostname(String hostname) {
- this.hostname = hostname;
- }
-
- public List<String> getIpAddresses() {
- return ipAddresses;
- }
-
- public List<String> getAdditionalIpAddresses() {
- return additionalIpAddresses;
- }
-
- public void setIpAddresses(List<String> ipAddresses) {
- this.ipAddresses = ipAddresses;
- }
-
- public void setAdditionalIpAddresses(List<String> additionalIpAddresses) {
- this.additionalIpAddresses = additionalIpAddresses;
- }
-
- public List<String> getAdditionalHostnames() {
- return additionalHostnames;
- }
-
- public void setAdditionalHostnames(List<String> additionalHostnames) {
- this.additionalHostnames = additionalHostnames;
- }
-
- public String getFlavor() {
- return flavor;
- }
-
- public void setFlavor(String flavor) {
- this.flavor = flavor;
- }
-
- public NodeResources getResources() {
- return resources;
- }
-
- public void setResources(NodeResources resources) {
- this.resources = resources;
- }
-
- public NodeResources getRequestedResources() {
- return requestedResources;
- }
-
- public void setRequestedResources(NodeResources requestedResources) {
- this.requestedResources = requestedResources;
- }
-
- public NodeMembership getMembership() {
- return membership;
- }
-
- public void setMembership(NodeMembership membership) {
- this.membership = membership;
- }
-
- public NodeOwner getOwner() {
- return owner;
- }
-
- public void setOwner(NodeOwner owner) {
- this.owner = owner;
- }
-
- public Integer getRestartGeneration() {
- return restartGeneration;
- }
-
- public void setRestartGeneration(Integer restartGeneration) {
- this.restartGeneration = restartGeneration;
- }
-
- public Integer getRebootGeneration() {
- return rebootGeneration;
- }
-
- public void setRebootGeneration(Integer rebootGeneration) {
- this.rebootGeneration = rebootGeneration;
- }
-
- public Integer getCurrentRestartGeneration() {
- return currentRestartGeneration;
- }
-
- public void setCurrentRestartGeneration(Integer currentRestartGeneration) {
- this.currentRestartGeneration = currentRestartGeneration;
- }
-
- public Integer getCurrentRebootGeneration() {
- return currentRebootGeneration;
- }
-
- public void setCurrentRebootGeneration(Integer currentRebootGeneration) {
- this.currentRebootGeneration = currentRebootGeneration;
- }
-
- public String getVespaVersion() {
- return vespaVersion;
- }
-
- public void setVespaVersion(String vespaVersion) {
- this.vespaVersion = vespaVersion;
- }
-
- public String getWantedVespaVersion() {
- return wantedVespaVersion;
- }
-
- public void setWantedVespaVersion(String wantedVespaVersion) {
- this.wantedVespaVersion = wantedVespaVersion;
- }
-
- public Boolean getDeferOsUpgrade() {
- return deferOsUpgrade;
- }
-
- public void setDeferOsUpgrade(Boolean deferOsUpgrade) {
- this.deferOsUpgrade = deferOsUpgrade;
- }
-
- public Integer getFailCount() {
- return failCount;
- }
-
- public void setFailCount(Integer failCount) {
- this.failCount = failCount;
- }
-
- public String getEnvironment() {
- return environment;
- }
-
- public void setEnvironment(String environment) {
- this.environment = environment;
- }
-
- public String getType() {
- return type;
- }
-
- public void setType(String type) {
- this.type = type;
- }
-
- public String getWantedDockerImage() {
- return wantedDockerImage;
- }
-
- public void setWantedDockerImage(String wantedDockerImage) {
- this.wantedDockerImage = wantedDockerImage;
- }
-
- public String getCurrentDockerImage() {
- return currentDockerImage;
- }
-
- public void setCurrentDockerImage(String currentDockerImage) {
- this.currentDockerImage = currentDockerImage;
- }
-
- public String getParentHostname() {
- return parentHostname;
- }
-
- public void setParentHostname(String parentHostname) {
- this.parentHostname = parentHostname;
- }
-
- public Boolean getWantToRetire() {
- return wantToRetire;
- }
-
- public Boolean getWantToDeprovision() { return wantToDeprovision; }
-
- public Boolean getWantToRebuild() {
- return wantToRebuild;
- }
-
- public void setWantToRetire(Boolean wantToRetire) {
- this.wantToRetire = wantToRetire;
- }
-
- public void setWantToDeprovision(Boolean wantToDeprovision) {
- this.wantToDeprovision = wantToDeprovision;
- }
-
- public void setWantToRebuild(Boolean wantToRebuild) {
- this.wantToRebuild = wantToRebuild;
- }
-
- public Boolean getDown() {
- return down;
- }
-
- public void setDown(Boolean down) {
- this.down = down;
- }
-
- public Integer getCost() {
- return cost;
- }
-
- public void setCost(Integer cost) {
- this.cost = cost;
- }
-
- public List<NodeHistory> getHistory() {
- return history;
- }
-
- public void setHistory(List<NodeHistory> history) {
- this.history = history;
- }
-
- public String getOrchestratorStatus() {
- return orchestratorStatus;
- }
-
- public void setOrchestratorStatus(String orchestratorStatus) {
- this.orchestratorStatus = orchestratorStatus;
- }
-
- public Long suspendedSinceMillis() {
- return suspendedSinceMillis;
- }
-
- public void setSuspendedSinceMillis(long suspendedSinceMillis) {
- this.suspendedSinceMillis = suspendedSinceMillis;
- }
-
- public String getCurrentOsVersion() {
- return currentOsVersion;
- }
-
- public void setCurrentOsVersion(String currentOsVersion) {
- this.currentOsVersion = currentOsVersion;
- }
-
- public String getWantedOsVersion() {
- return wantedOsVersion;
- }
-
- public void setWantedOsVersion(String wantedOsVersion) {
- this.wantedOsVersion = wantedOsVersion;
- }
-
- public Long getCurrentFirmwareCheck() {
- return currentFirmwareCheck;
- }
-
- public void setCurrentFirmwareCheck(Long currentFirmwareCheck) {
- this.currentFirmwareCheck = currentFirmwareCheck;
- }
-
- public Long getWantedFirmwareCheck() {
- return wantedFirmwareCheck;
- }
-
- public void setWantedFirmwareCheck(Long wantedFirmwareCheck) {
- this.wantedFirmwareCheck = wantedFirmwareCheck;
- }
-
- @JsonIgnore
- public Map<String, JsonNode> getReports() {
- return reports == null ? Map.of() : reports;
- }
-
- @JsonGetter("reports")
- public Map<String, JsonNode> getReportsOrNull() { return reports; }
-
- public void setReports(Map<String, JsonNode> reports) {
- this.reports = reports;
- }
-
- public String getModelName() {
- return modelName;
- }
-
- public void setModelName(String modelName) {
- this.modelName = modelName;
- }
-
- public String getReservedTo() { return reservedTo; }
-
- public void setReservedTo(String reservedTo) { this.reservedTo = reservedTo; }
-
- public String getExclusiveTo() { return exclusiveTo; }
-
- public void setExclusiveTo(String exclusiveTo) { this.exclusiveTo = exclusiveTo; }
-
- public String getExclusiveToClusterType() { return exclusiveToClusterType; }
-
- public void setExclusiveToClusterType(String exclusiveToClusterType) { this.exclusiveToClusterType = exclusiveToClusterType; }
-
- public String getSwitchHostname() {
- return switchHostname;
- }
-
- public void setSwitchHostname(String switchHostname) {
- this.switchHostname = switchHostname;
- }
-
- public String getCloudAccount() {
- return cloudAccount;
- }
-
- public void setCloudAccount(String cloudAccount) {
- this.cloudAccount = cloudAccount;
- }
-
- public String getWireguardPubKey() { return wireguardPubKey; }
-
- public void setWireguardPubKey(String wireguardPubKey) { this.wireguardPubKey = wireguardPubKey; }
-
- public String getArchiveUri() { return archiveUri; }
-
- public void setArchiveUri(String archiveUri) { this.archiveUri = archiveUri; }
-
- // --- Helper methods for code that (wrongly) consume this directly
-
- public boolean hasType(NodeType type) {
- return type.name().equals(getType());
- }
-
- public boolean hasState(NodeState state) {
- return state.name().equals(getState());
- }
-
- // --- end
-
-
- @Override
- public String toString() {
- return "NodeRepositoryNode{" +
- "url='" + url + '\'' +
- ", id='" + id + '\'' +
- ", state='" + state + '\'' +
- ", hostname='" + hostname + '\'' +
- ", ipAddresses=" + ipAddresses +
- ", additionalIpAddresses=" + additionalIpAddresses +
- ", additionalHostnames=" + additionalHostnames +
- ", flavor='" + flavor + '\'' +
- ", resources=" + resources +
- ", requestedResources=" + requestedResources +
- ", membership=" + membership +
- ", owner=" + owner +
- ", restartGeneration=" + restartGeneration +
- ", rebootGeneration=" + rebootGeneration +
- ", currentRestartGeneration=" + currentRestartGeneration +
- ", currentRebootGeneration=" + currentRebootGeneration +
- ", vespaVersion='" + vespaVersion + '\'' +
- ", wantedVespaVersion='" + wantedVespaVersion + '\'' +
- ", currentOsVersion='" + currentOsVersion + '\'' +
- ", wantedOsVersion='" + wantedOsVersion + '\'' +
- ", deferOsUpgrade=" + deferOsUpgrade +
- ", currentFirmwareCheck=" + currentFirmwareCheck +
- ", wantedFirmwareCheck=" + wantedFirmwareCheck +
- ", failCount=" + failCount +
- ", environment='" + environment + '\'' +
- ", type='" + type + '\'' +
- ", wantedDockerImage='" + wantedDockerImage + '\'' +
- ", currentDockerImage='" + currentDockerImage + '\'' +
- ", parentHostname='" + parentHostname + '\'' +
- ", wantToRetire=" + wantToRetire +
- ", wantToDeprovision=" + wantToDeprovision +
- ", wantToRebuild=" + wantToRebuild +
- ", down=" + down +
- ", cost=" + cost +
- ", history=" + history +
- ", orchestratorStatus='" + orchestratorStatus + '\'' +
- ", suspendedSinceMillis=" + suspendedSinceMillis +
- ", reports=" + reports +
- ", modelName='" + modelName + '\'' +
- ", reservedTo='" + reservedTo + '\'' +
- ", exclusiveTo='" + exclusiveTo + '\'' +
- ", exclusiveToClusterType='" + exclusiveToClusterType + '\'' +
- ", switchHostname='" + switchHostname + '\'' +
- ", cloudAccount='" + cloudAccount + '\'' +
- ", wireguardPubKey='" + wireguardPubKey + '\'' +
- ", archiveUri='" + archiveUri + '\'' +
- '}';
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeResources.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeResources.java
deleted file mode 100644
index a14bf599762..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeResources.java
+++ /dev/null
@@ -1,164 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * @author freva
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class NodeResources {
-
- @JsonProperty
- private Double vcpu;
- @JsonProperty
- private Double memoryGb;
- @JsonProperty
- private Double diskGb;
- @JsonProperty
- private Double bandwidthGbps;
- @JsonProperty
- private String diskSpeed;
- @JsonProperty
- private String storageType;
- @JsonProperty
- private String architecture;
- @JsonProperty
- private Double gpuCount;
- @JsonProperty
- private Double gpuMemoryGb;
-
-
- public Double getVcpu() {
- return vcpu;
- }
-
- public void setVcpu(Double vcpu) {
- this.vcpu = vcpu;
- }
-
- public Double getMemoryGb() {
- return memoryGb;
- }
-
- public void setMemoryGb(Double memoryGb) {
- this.memoryGb = memoryGb;
- }
-
- public Double getDiskGb() {
- return diskGb;
- }
-
- public void setDiskGb(Double diskGb) {
- this.diskGb = diskGb;
- }
-
- public Double getBandwidthGbps() {
- return bandwidthGbps;
- }
-
- public void setBandwidthGbps(Double bandwidthGbps) {
- this.bandwidthGbps = bandwidthGbps;
- }
-
- public String getDiskSpeed() {
- return diskSpeed;
- }
-
- public void setDiskSpeed(String diskSpeed) {
- this.diskSpeed = diskSpeed;
- }
-
- public String getStorageType() {
- return storageType;
- }
-
- public void setStorageType(String storageType) {
- this.storageType = storageType;
- }
-
- public String getArchitecture() {
- return architecture;
- }
-
- public void setArchitecture(String architecture) {
- this.architecture = architecture;
- }
-
- public Double getGpuCount(){
- return gpuCount;
- }
-
- public void setGpuCount(Double gpuCount) {
- this.gpuCount = gpuCount;
- }
-
- public Double getGpuMemoryGb() {
- return gpuMemoryGb;
- }
-
- public void setGpuMemoryGb(Double gpuMemoryGb) {
- this.gpuMemoryGb = gpuMemoryGb;
- }
-
- public com.yahoo.config.provision.NodeResources toNodeResources() {
- return new com.yahoo.config.provision.NodeResources(vcpu, memoryGb, diskGb, bandwidthGbps,
- toDiskSpeed(diskSpeed),
- toStorageType(storageType),
- toArchitecture(architecture),
- toGpu(gpuCount, gpuMemoryGb));
- }
-
- private com.yahoo.config.provision.NodeResources.DiskSpeed toDiskSpeed(String diskSpeed) {
- switch (diskSpeed) {
- case "fast" : return com.yahoo.config.provision.NodeResources.DiskSpeed.fast;
- case "slow" : return com.yahoo.config.provision.NodeResources.DiskSpeed.slow;
- case "any" : return com.yahoo.config.provision.NodeResources.DiskSpeed.any;
- default : throw new IllegalArgumentException("Unknown disk speed '" + diskSpeed + "'");
- }
- }
-
- private com.yahoo.config.provision.NodeResources.StorageType toStorageType(String storageType) {
- switch (storageType) {
- case "remote" : return com.yahoo.config.provision.NodeResources.StorageType.remote;
- case "local" : return com.yahoo.config.provision.NodeResources.StorageType.local;
- case "any" : return com.yahoo.config.provision.NodeResources.StorageType.any;
- default : throw new IllegalArgumentException("Unknown storage type '" + storageType + "'");
- }
- }
-
- private com.yahoo.config.provision.NodeResources.Architecture toArchitecture(String architecture) {
- switch (architecture) {
- case "arm64" : return com.yahoo.config.provision.NodeResources.Architecture.arm64;
- case "x86_64" : return com.yahoo.config.provision.NodeResources.Architecture.x86_64;
- case "any" : return com.yahoo.config.provision.NodeResources.Architecture.any;
- default : throw new IllegalArgumentException("Unknown architecture '" + architecture + "'");
- }
- }
-
- private com.yahoo.config.provision.NodeResources.GpuResources toGpu(Double gpuCount, Double gpuMemoryGb) {
- // these are either both null or both have a value. using OR to silence inspection.
- // we also cast the double to an integer. assuming this must be OK as we are going
- // from NodeResources -> JSON -> NodeResources
- if (gpuCount == null || gpuMemoryGb == null) return com.yahoo.config.provision.NodeResources.GpuResources.getDefault();
- return new com.yahoo.config.provision.NodeResources.GpuResources(gpuCount.intValue(), gpuMemoryGb.intValue());
- }
-
- @Override
- public String toString() {
- return "NodeResources{" +
- "vcpu=" + vcpu +
- ", memoryGb=" + memoryGb +
- ", diskGb=" + diskGb +
- ", bandwidthGbps=" + bandwidthGbps +
- ", diskSpeed='" + diskSpeed + '\'' +
- ", storageType='" + storageType + '\'' +
- ", architecture='" + architecture + '\'' +
- ", gpuCount='" + gpuCount + '\'' +
- ", gpuMemoryGb='" + gpuMemoryGb + '\'' +
- '}';
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeState.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeState.java
deleted file mode 100644
index 6321fb0dc83..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeState.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-/**
- * @author bjorncs
- */
-public enum NodeState {
-
- provisioned,
- ready,
- reserved,
- active,
- inactive,
- dirty,
- failed,
- parked,
- deprovisioned,
- breakfixed
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeTargetVersions.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeTargetVersions.java
deleted file mode 100644
index 2222b89a608..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeTargetVersions.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-import java.util.Map;
-
-/**
- * @author mpolden
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class NodeTargetVersions {
-
- @JsonProperty("versions")
- private final Map<NodeType, String> vespaVersions;
-
- @JsonProperty("osVersions")
- private final Map<NodeType, String> osVersions;
-
- @JsonCreator
- public NodeTargetVersions(@JsonProperty("versions") Map<NodeType, String> vespaVersions,
- @JsonProperty("osVersions") Map<NodeType, String> osVersions) {
- this.vespaVersions = Map.copyOf(vespaVersions);
- this.osVersions = Map.copyOf(osVersions);
- }
-
- public Map<NodeType, String> vespaVersions() {
- return vespaVersions;
- }
-
- public Map<NodeType, String> osVersions() {
- return osVersions;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeType.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeType.java
deleted file mode 100644
index 845400ff978..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeType.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-/**
- * The possible types of nodes in the node repository
- *
- * @author bjorncs
- */
-public enum NodeType {
-
- /** A node to be assigned to a tenant to run application workloads */
- tenant,
-
- /** A host of a set of (docker) tenant nodes */
- host,
-
- /** Nodes running the shared proxy layer */
- proxy,
-
- /** A host of a (docker) proxy node */
- proxyhost,
-
- /** A config server */
- config,
-
- /** A host of a (docker) config server node */
- confighost,
-
- /** A controller */
- controller,
-
- /** A host of a (docker) controller node */
- controllerhost
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeUpgrade.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeUpgrade.java
deleted file mode 100644
index f7edad1d3c2..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/NodeUpgrade.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * @author mpolden
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class NodeUpgrade {
-
- @JsonProperty("version")
- private final String version;
-
- @JsonProperty("osVersion")
- private final String osVersion;
-
- @JsonProperty("force")
- private final boolean force;
-
- @JsonCreator
- public NodeUpgrade(@JsonProperty("version") String version, @JsonProperty("osVersion") String osVersion,
- @JsonProperty("force") boolean force) {
- this.version = version;
- this.osVersion = osVersion;
- this.force = force;
- }
-
- public String getVersion() {
- return version;
- }
-
- public String getOsVersion() {
- return osVersion;
- }
-
- public boolean isForce() {
- return force;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ProvisionResource.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ProvisionResource.java
deleted file mode 100644
index 404dc932e05..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ProvisionResource.java
+++ /dev/null
@@ -1,134 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.yahoo.config.provision.TenantName;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.GET;
-import javax.ws.rs.HeaderParam;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.MediaType;
-import java.util.Collection;
-
-/**
- * @author mortent
- * @author mpolden
- * @author smorgrav
- */
-@Consumes(MediaType.APPLICATION_JSON)
-public interface ProvisionResource {
-
- @POST
- @Path("/node")
- String addNodes(Collection<NodeRepositoryNode> node);
-
- @DELETE
- @Path("/node/{hostname}")
- String deleteNode(@PathParam("hostname") String hostname);
-
- @GET
- @Path("/node/{hostname}")
- NodeRepositoryNode getNode(@PathParam("hostname") String hostname);
-
- @POST
- @Path("/node/{hostname}")
- String patchNode(@PathParam("hostname") String hostname,
- NodeRepositoryNode patchValues,
- @HeaderParam("X-HTTP-Method-Override") String patchOverride);
-
- @GET
- @Path("/node/")
- NodeList listNodes(@QueryParam("recursive") boolean recursive, @QueryParam("includeDeprovisioned") boolean includeDeprovisioned);
-
- @GET
- @Path("/node/")
- NodeList listNodes(@QueryParam("application") String applicationString,
- @QueryParam("recursive") boolean recursive);
-
- @GET
- @Path("/node/")
- NodeList listNodes(@QueryParam("recursive") boolean recursive,
- @QueryParam("hostname") String hostnamesString);
-
- @Path("/node/")
- NodeList listNodesWithParent(@QueryParam("recursive") boolean recursive,
- @QueryParam("parentHost") String parentHostname);
-
- @GET
- @Path("/application/{application}")
- ApplicationData getApplication(@PathParam("application") String applicationId);
-
- @POST
- @Path("/application/{application}")
- String patchApplication(@PathParam("application") String applicationId, ApplicationPatch applicationPatch,
- @HeaderParam("X-HTTP-Method-Override") String patchOverride);
-
- @GET
- @Path("/stats")
- NodeRepoStatsData getStats();
-
- @PUT
- @Path("/state/{state}/{hostname}")
- String setState(@PathParam("state") NodeState state, @PathParam("hostname") String hostname);
-
- @POST
- @Path("/command/reboot")
- String reboot(@QueryParam("hostname") String hostname);
-
- @POST
- @Path("/command/restart")
- String restart(@QueryParam("hostname") String hostname);
-
- @GET
- @Path("/maintenance/")
- MaintenanceJobList listMaintenanceJobs();
-
- @POST
- @Path("/maintenance/inactive/{jobname}")
- String disableMaintenanceJob(@PathParam("jobname") String jobname);
-
- @DELETE
- @Path("/maintenance/inactive/{jobname}")
- String enableMaintenanceJob(@PathParam("jobname") String jobname);
-
- @POST
- @Path("/upgrade/{nodeType}")
- String upgrade(@PathParam("nodeType") NodeType nodeType, NodeUpgrade nodeUpgrade,
- @HeaderParam("X-HTTP-Method-Override") String patchOverride);
-
- @GET
- @Path("/upgrade/")
- NodeTargetVersions upgrade();
-
- @POST
- @Path("/upgrade/firmware")
- String requestFirmwareChecks();
-
- @DELETE
- @Path("/upgrade/firmware")
- String cancelFirmwareChecks();
-
- @GET
- @Path("/archive")
- ArchiveList listArchives();
-
- @POST
- @Path("/archive/{tenant}")
- String patchArchive(@PathParam("tenant") TenantName tenant, ArchivePatch archivePatch,
- @HeaderParam("X-HTTP-Method-Override") String patchOverride);
-
- @DELETE
- @Path("/archive/{tenant}")
- String removeArchiveUri(@PathParam("tenant") TenantName tenant);
-
- @GET
- @Path("/capacity")
- Capacity capacity(@QueryParam("json") boolean json,
- @QueryParam("hosts") String hostList);
-}
-
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/RestartFilter.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/RestartFilter.java
deleted file mode 100644
index c8623d1b4fa..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/RestartFilter.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
-
-import java.util.Optional;
-
-/**
- * Attributes to filter when restarting nodes in a deployment.
- * If all attributes are empty, all nodes are restarted.
- * Used in {@link ConfigServer#restart(DeploymentId, RestartFilter)}
- *
- * @author olaa
- */
-public class RestartFilter {
-
- private Optional<HostName> hostName = Optional.empty();
- private Optional<ClusterSpec.Type> clusterType = Optional.empty();
- private Optional<ClusterSpec.Id> clusterId = Optional.empty();
-
- public Optional<HostName> getHostName() {
- return hostName;
- }
-
- public Optional<ClusterSpec.Type> getClusterType() {
- return clusterType;
- }
-
- public Optional<ClusterSpec.Id> getClusterId() {
- return clusterId;
- }
-
- public RestartFilter withHostName(Optional<HostName> hostName) {
- this.hostName = hostName;
- return this;
- }
-
- public RestartFilter withClusterType(Optional<ClusterSpec.Type> clusterType) {
- this.clusterType = clusterType;
- return this;
- }
-
- public RestartFilter withClusterId(Optional<ClusterSpec.Id> clusterId) {
- this.clusterId = clusterId;
- return this;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ScalingEventData.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ScalingEventData.java
deleted file mode 100644
index 75637bcc13c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/ScalingEventData.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
-
-import java.time.Instant;
-import java.util.Optional;
-
-/**
- * @author bratseth
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class ScalingEventData {
-
- @JsonProperty("from")
- public ClusterResourcesData from;
-
- @JsonProperty("to")
- public ClusterResourcesData to;
-
- @JsonProperty("at")
- public Long at;
-
- @JsonProperty("completion")
- public Long completion;
-
- public Cluster.ScalingEvent toScalingEvent() {
- return new Cluster.ScalingEvent(from.toClusterResources(), to.toClusterResources(), Instant.ofEpochMilli(at),
- toOptionalInstant(completion));
- }
-
- private Optional<Instant> toOptionalInstant(Long epochMillis) {
- if (epochMillis == null) return Optional.empty();
- return Optional.of(Instant.ofEpochMilli(epochMillis));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/package-info.java
deleted file mode 100644
index 0489de0f4a9..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/noderepository/package-info.java
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author bjorncs
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.noderepository;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/AccountId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/AccountId.java
deleted file mode 100644
index 660c57babac..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/AccountId.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import ai.vespa.validation.StringWrapper;
-
-public class AccountId extends StringWrapper<AccountId> {
-
- public AccountId(String value) {
- super(value);
- if (value.isBlank()) throw new IllegalArgumentException("id must be non-blank");
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ApplicationSummary.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ApplicationSummary.java
deleted file mode 100644
index 9ad6afddae0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ApplicationSummary.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-
-import java.time.Instant;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * A summary of activity in an application.
- *
- * @author mpolden
- */
-public class ApplicationSummary {
-
- private final ApplicationId application;
- private final Optional<Instant> lastQueried;
- private final Optional<Instant> lastWritten;
- private final Optional<Instant> lastBuilt;
- private final Map<DeploymentId, Metric> metrics;
-
- public ApplicationSummary(ApplicationId application, Optional<Instant> lastQueried, Optional<Instant> lastWritten,
- Optional<Instant> lastBuilt, Map<DeploymentId, Metric> metrics) {
- this.application = Objects.requireNonNull(application);
- this.lastQueried = Objects.requireNonNull(lastQueried);
- this.lastWritten = Objects.requireNonNull(lastWritten);
- this.lastBuilt = Objects.requireNonNull(lastBuilt);
- this.metrics = Map.copyOf(Objects.requireNonNull(metrics));
- }
-
- public ApplicationId application() {
- return application;
- }
-
- public Optional<Instant> lastQueried() {
- return lastQueried;
- }
-
- public Optional<Instant> lastWritten() {
- return lastWritten;
- }
-
- public Optional<Instant> lastBuilt() {
- return lastBuilt;
- }
-
- public Map<DeploymentId, Metric> metrics() {
- return metrics;
- }
-
- public static class Metric {
-
- private final double documentCount;
- private final double queriesPerSecond;
- private final double writesPerSecond;
-
- public Metric(double documentCount, double queriesPerSecond, double writesPerSecond) {
- this.documentCount = documentCount;
- this.queriesPerSecond = queriesPerSecond;
- this.writesPerSecond = writesPerSecond;
- }
-
- public double documentCount() {
- return documentCount;
- }
-
- public double queriesPerSecond() {
- return queriesPerSecond;
- }
-
- public double writesPerSecond() {
- return writesPerSecond;
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/BillingInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/BillingInfo.java
deleted file mode 100644
index 801330bc8d0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/BillingInfo.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import java.util.Objects;
-import java.util.StringJoiner;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Information pertinent to billing a tenant for use of hosted Vespa services.
- *
- * @author jonmv
- */
-public class BillingInfo {
-
- private final String customerId;
- private final String productCode;
-
- /** Creates a new BillingInfo with the given data. Assumes data has already been validated. */
- public BillingInfo(String customerId, String productCode) {
- this.customerId = requireNonNull(customerId);
- this.productCode = requireNonNull(productCode);
- }
-
- public String customerId() {
- return customerId;
- }
-
- public String productCode() {
- return productCode;
- }
-
- @Override
- public String toString() {
- return new StringJoiner(", ", BillingInfo.class.getSimpleName() + "[", "]")
- .add("customerId='" + customerId + "'")
- .add("productCode='" + productCode + "'")
- .toString();
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if ( ! (o instanceof BillingInfo)) return false;
- BillingInfo that = (BillingInfo) o;
- return Objects.equals(customerId, that.customerId) &&
- Objects.equals(productCode, that.productCode);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(customerId, productCode);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Contact.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Contact.java
deleted file mode 100644
index ac532378923..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Contact.java
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import java.net.URI;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Contact information for a tenant.
- *
- * @author mpolden
- */
-public class Contact {
-
- private final URI url;
- private final URI propertyUrl;
- private final URI issueTrackerUrl;
- private final List<List<String>> persons;
- private final String queue;
- private final Optional<String> component;
-
- public Contact(URI url, URI propertyUrl, URI issueTrackerUrl, List<List<String>> persons, String queue, Optional<String> component) {
- this.propertyUrl = Objects.requireNonNull(propertyUrl, "propertyUrl must be non-null");
- this.url = Objects.requireNonNull(url, "url must be non-null");
- this.issueTrackerUrl = Objects.requireNonNull(issueTrackerUrl, "issueTrackerUrl must be non-null");
- this.persons = List.copyOf(Objects.requireNonNull(persons, "persons must be non-null"));
- this.queue = queue;
- this.component = component;
- }
-
- /** URL to this */
- public URI url() {
- return url;
- }
-
- /** URL to information about this property */
- public URI propertyUrl() {
- return propertyUrl;
- }
-
- /** URL to this contacts's issue tracker */
- public URI issueTrackerUrl() {
- return issueTrackerUrl;
- }
-
- /** Nested list of persons representing this. First level represents that person's rank in the corporate dystopia. */
- public List<List<String>> persons() {
- return persons;
- }
-
- public String queue() {
- return queue;
- }
-
- public Optional<String> component() {
- return component;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Contact contact = (Contact) o;
- return Objects.equals(url, contact.url) &&
- Objects.equals(propertyUrl, contact.propertyUrl) &&
- Objects.equals(issueTrackerUrl, contact.issueTrackerUrl) &&
- Objects.equals(persons, contact.persons) &&
- Objects.equals(queue, contact.queue) &&
- Objects.equals(component, contact.component);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(url, propertyUrl, issueTrackerUrl, persons);
- }
-
- @Override
- public String toString() {
- return "Contact{" +
- "url=" + url +
- ", propertyUrl=" + propertyUrl +
- ", issueTrackerUrl=" + issueTrackerUrl +
- ", persons=" + persons +
- ", queue=" + queue +
- (component.isPresent() ? ", component=" + component.get() : "") +
- '}';
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ContactRetriever.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ContactRetriever.java
deleted file mode 100644
index 40287a10bb3..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ContactRetriever.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-
-import java.util.Optional;
-
-/**
- * @author olaa
- */
-public interface ContactRetriever {
- Contact getContact(Optional<PropertyId> propertyId);
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentFailureMails.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentFailureMails.java
deleted file mode 100644
index 72728966dbc..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentFailureMails.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-
-import java.util.Collection;
-
-/**
- * Used to create mails for different kinds of deployment failure.
- *
- * @author jonmv
- */
-public class DeploymentFailureMails {
-
- private final ConsoleUrls consoleUrls;
-
- public DeploymentFailureMails(ConsoleUrls consoleUrls) {
- this.consoleUrls = consoleUrls;
- }
-
- public Mail nodeAllocationFailure(RunId id, Collection<String> recipients) {
- return mail(id, recipients, " due to node allocation failure",
- "as your node resource request could not be " +
- "fulfilled for your tenant. Please contact Vespa Cloud support.");
- }
-
- public Mail deploymentFailure(RunId id, Collection<String> recipients) {
- return mail(id, recipients, " deployment",
- "and any previous deployment in the zone is unaffected. " +
- "This is usually due to an invalid application configuration. " +
- "Please review warnings and errors in the deployment job log.");
- }
-
- public Mail installationFailure(RunId id, Collection<String> recipients) {
- return mail(id, recipients, "installation",
- "as nodes were not able to deploy to the new configuration. " +
- "This is often due to a misconfiguration of the components of an " +
- "application, where one or more of these can not be instantiated. " +
- "Please check the Vespa log for errors, and contact Vespa Cloud " +
- "support if unable to resolve these.");
- }
-
- public Mail testFailure(RunId id, Collection<String> recipients) {
- return mail(id, recipients, "tests",
- "as one or more verification tests against the deployment failed. " +
- "Please review test output in the deployment job log.");
- }
-
- public Mail systemError(RunId id, Collection<String> recipients) {
- return mail(id, recipients, "due to system error",
- "as something in the deployment framework went wrong. Such errors are " +
- "usually transient. Please contact Vespa Cloud support if the problem persists.");
- }
-
- private Mail mail(RunId id, Collection<String> recipients, String summaryDetail, String messageDetail) {
- return new Mail(recipients,
- String.format("Vespa application %s: %s failing %s",
- id.application(),
- jobToString(id.type()),
- summaryDetail),
- String.format("%s for the Vespa application '%s' just failed, %s\n" +
- "Details about the job can be viewed at %s.\n" +
- "If you require further assistance, please contact the Vespa team at %s.",
- jobToString(id.type()),
- id.application(),
- messageDetail,
- consoleUrls.deploymentRun(id),
- consoleUrls.support()));
- }
-
- private String jobToString(JobType type) {
- if (type.isSystemTest())
- return "System test";
- if (type.isStagingTest())
- return "Staging test";
- return (type.isDeployment() ? "Deployment to " : "Verification test of ") + type.zone().region();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentIssues.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentIssues.java
deleted file mode 100644
index 0c0f1b8e6e4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/DeploymentIssues.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-
-import java.time.Duration;
-import java.util.Collection;
-import java.util.Optional;
-
-/**
- * Represents the people responsible for keeping Vespa up and running in a given organization, etc..
- *
- * @author jonmv
- */
-public interface DeploymentIssues {
-
- IssueId fileUnlessOpen(Optional<IssueId> issueId, ApplicationId applicationId, AccountId assigneeId, User assignee, Contact contact);
-
- IssueId fileUnlessOpen(Collection<ApplicationId> applicationIds, Version version);
-
- void escalateIfInactive(IssueId issueId, Duration maxInactivity, Optional<Contact> contact);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Issue.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Issue.java
deleted file mode 100644
index 11f497ce2ca..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Issue.java
+++ /dev/null
@@ -1,120 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Represents an issue which needs to reported, typically from the controller, to a responsible party,
- * the identity of which is determined by the propertyId and, possibly, assignee fields.
- *
- * @author jonmv
- */
-public class Issue {
-
- private final String summary;
- private final String description;
- private final List<String> labels;
- private final User assignee;
- private final AccountId assigneeId;
- private final Type type;
- private final String queue;
- private final Optional<String> component;
-
- private Issue(String summary, String description, List<String> labels, User assignee,
- AccountId assigneeId, Type type, String queue, Optional<String> component) {
- if (summary.isEmpty()) throw new IllegalArgumentException("Issue summary can not be empty!");
- if (description.isEmpty()) throw new IllegalArgumentException("Issue description can not be empty!");
-
- this.summary = summary;
- this.description = description;
- this.labels = List.copyOf(labels);
- this.assignee = assignee;
- this.assigneeId = assigneeId;
- this.type = type;
- this.queue = queue;
- this.component = component;
- }
-
- public Issue(String summary, String description, String queue, Optional<String> component) {
- this(summary, description, Collections.emptyList(), null, null, Type.defect, queue, component);
- }
-
- public Issue append(String appendage) {
- return new Issue(summary, description + appendage, labels, assignee, assigneeId, type, queue, component);
- }
-
- public Issue with(String label) {
- List<String> labels = new ArrayList<>(this.labels);
- labels.add(label);
- return new Issue(summary, description, labels, assignee, assigneeId, type, queue, component);
- }
-
- public Issue with(List<String> labels) {
- List<String> newLabels = new ArrayList<>(this.labels);
- newLabels.addAll(labels);
- return new Issue(summary, description, newLabels, assignee, assigneeId, type, queue, component);
- }
-
- public Issue with(AccountId assigneeId) {
- return new Issue(summary, description, labels, null, assigneeId, type, queue, component);
- }
-
- public Issue with(User assignee) {
- return new Issue(summary, description, labels, assignee, null, type, queue, component);
- }
-
- public Issue with(Type type) {
- return new Issue(summary, description, labels, assignee, assigneeId, type, queue, component);
- }
-
- public Issue in(String queue) {
- return new Issue(summary, description, labels, assignee, assigneeId, type, queue, Optional.empty());
- }
-
- public Issue withoutComponent() {
- return new Issue(summary, description, labels, assignee, assigneeId, type, queue, Optional.empty());
- }
-
- public String summary() {
- return summary;
- }
-
- public String description() {
- return description;
- }
-
- public List<String> labels() {
- return labels;
- }
-
- public Optional<User> assignee() { return Optional.ofNullable(assignee);
- }
-
- public Optional<AccountId> assigneeId() {
- return Optional.ofNullable(assigneeId);
- }
-
- public Type type() {
- return type;
- }
-
- public String queue() {
- return queue;
- }
-
- public Optional<String> component() {
- return component;
- }
-
- public enum Type {
-
- defect, // A defect which needs fixing.
- task, // A task the humans must perform.
- operationalTask // SRE and operational tasks.
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueHandler.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueHandler.java
deleted file mode 100644
index c810eb5d6f5..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueHandler.java
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-
-import com.yahoo.vespa.hosted.controller.api.integration.jira.JiraIssue;
-
-import java.io.InputStream;
-import java.time.Duration;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Supplier;
-
-/**
- * @author jonmv
- */
-public interface IssueHandler {
-
- /**
- * File an issue with its given property or the default, and with the specific assignee, if present.
- *
- * @param issue The issue to file.
- * @return ID of the created issue.
- */
- IssueId file(Issue issue);
-
- /**
- * Returns all open issues similar to the given.
- *
- * @param issue The issue to search for; relevant fields are the summary and the owner (propertyId).
- * @return All open, similar issues.
- */
- List<IssueInfo> findAllBySimilarity(Issue issue);
-
- /**
- * Returns the ID of this issue, if it exists and is open, based on a similarity search.
- *
- * @param issue The issue to search for; relevant fields are the summary and the owner (propertyId).
- * @return ID of the issue, if it is found.
- */
- default Optional<IssueId> findBySimilarity(Issue issue) {
- return findAllBySimilarity(issue).stream().findFirst().map(IssueInfo::id);
- }
-
- /**
- * Update the description of the issue with the given ID.
- *
- * @param issueId ID of the issue to comment on.
- * @param description The updated description.
- */
- void update(IssueId issueId, String description);
-
- /**
- * Add a comment to the issue with the given ID.
- *
- * @param issueId ID of the issue to comment on.
- * @param comment The comment to add.
- */
- void commentOn(IssueId issueId, String comment);
-
- /**
- * Returns whether the issue is still under investigation.
- *
- * @param issueId ID of the issue to examine.
- * @return Whether the given issue is under investigation.
- */
- boolean isOpen(IssueId issueId);
-
- /**
- * Returns whether there has been significant activity on the issue within the given duration.
- *
- * @param issueId ID of the issue to examine.
- * @return Whether the given issue is actively worked on.
- */
- boolean isActive(IssueId issueId, Duration maxInactivity);
-
- /**
- * Returns the user assigned to the given issue, if any.
- *
- * @param issueId ID of the issue for which to find the assignee.
- * @return The user responsible for fixing the given issue, if found.
- */
- Optional<User> assigneeOf(IssueId issueId);
-
- /**
- * Returns the account id assigned to the given issue, if any.
- *
- * @param issueId ID of the issue for which to find the assignee.
- * @return The account id of the user responsible for fixing the given issue, if found.
- */
- Optional<AccountId> assigneeIdOf(IssueId issueId);
-
- /**
- * Reassign the issue with the given ID to the given user, and returns the outcome of this.
- *
- * @param issueId ID of the issue to be reassigned.
- * @param assignee User to which the issue shall be assigned.
- * @return Whether the reassignment was successful.
- */
- boolean reassign(IssueId issueId, User assignee);
-
- /**
- * Reassign the issue with the given ID to the given user, and returns the outcome of this.
- *
- * @param issueId ID of the issue to be watched.
- * @param watcher watcher to add to the issue.
- * @return Whether adding the watcher was successful.
- */
- boolean addWatcher(IssueId issueId, String watcher);
-
- /**
- * Escalate an issue filed with the given property.
- *
- * @param issueId ID of the issue to escalate.
- * @return User that was assigned issue as a result of the escalation, if any
- */
- Optional<User> escalate(IssueId issueId, Contact contact);
-
- /**
- * Returns whether there exists an issue with an exactly matching summary.
- *
- * @param issue The summary of the issue.
- * @return Whether the issue exists.
- */
- boolean issueExists(Issue issue);
-
- /**
- * Returns information about project identified by the project key
- *
- * @param projectKey The project key to find information for
- * @return Project info for project
- * @throws RuntimeException exception if project not found
- */
- ProjectInfo projectInfo(String projectKey);
-
- /** Upload an attachment to the issue, with indicated filename, from the given input stream. */
- void addAttachment(IssueId id, String filename, Supplier<InputStream> contentAsStream);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueId.java
deleted file mode 100644
index 7553bde74a6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueId.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import java.util.Objects;
-
-/**
- * Used to identify issues stored in some issue tracking system.
- * The {@code value()} and {@code from()} methods should be inverses.
- *
- * @author jonmv
- */
-public class IssueId {
-
- protected final String id;
-
- protected IssueId(String id) {
- this.id = id;
- }
-
- public static IssueId from(String value) {
- if (value.isEmpty())
- throw new IllegalArgumentException("Can not make an IssueId from an empty value.");
-
- return new IssueId(value);
- }
-
- public String value() {
- return id;
- }
-
- @Override
- public String toString() {
- return value();
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- IssueId issueId = (IssueId) o;
- return Objects.equals(id, issueId.id);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id);
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueInfo.java
deleted file mode 100644
index 4f0b8ac14cf..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/IssueInfo.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-
-import java.time.Instant;
-import java.util.Optional;
-
-/**
- * Information about a stored issue.
- *
- * @author jonmv
- */
-public class IssueInfo {
-
- private final IssueId id;
- private final Instant updated;
- private final Status status;
- private final AccountId assigneeId;
-
- public IssueInfo(IssueId id, Instant updated, Status status, AccountId assigneeId) {
- this.id = id;
- this.updated = updated;
- this.status = status;
- this.assigneeId = assigneeId;
- }
-
- public IssueId id() {
- return id;
- }
-
- public Instant updated() {
- return updated;
- }
-
- public Status status() {
- return status;
- }
-
- public Optional<AccountId> assigneeId() {
- return Optional.ofNullable(assigneeId);
- }
-
- public enum Status {
-
- toDo("To Do"),
- inProgress("In Progress"),
- done("Done"),
- noCategory("No Category");
-
- private final String value;
-
- Status(String value) { this.value = value; }
-
- public static Status fromValue(String value) {
- for (Status status : Status.values())
- if (status.value.equals(value))
- return status;
- throw new IllegalArgumentException(value + " is not a valid status.");
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Mail.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Mail.java
deleted file mode 100644
index c8c3f2db14d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Mail.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.prelude.IndexFacts;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * A message with a subject and a nonempty set of recipients.
- *
- * @author jonmv
- */
-public class Mail {
-
- private final Collection<String> recipients;
- private final String subject;
- private final String message;
- private final Optional<String> htmlMessage;
-
- public Mail(Collection<String> recipients, String subject, String message) {
- this(recipients, subject, message, Optional.empty());
- }
-
- public Mail(Collection<String> recipients, String subject, String message, String htmlMessage) {
- this(recipients, subject, message, Optional.of(htmlMessage));
- }
-
- Mail(Collection<String> recipients, String subject, String message, Optional<String> htmlMessage) {
- if (recipients.isEmpty())
- throw new IllegalArgumentException("Empty recipient list is not allowed.");
- recipients.forEach(Objects::requireNonNull);
- this.recipients = List.copyOf(recipients);
- this.subject = Objects.requireNonNull(subject);
- this.message = Objects.requireNonNull(message);
- this.htmlMessage = Objects.requireNonNull(htmlMessage);
- }
-
- public Collection<String> recipients() { return recipients; }
- public String subject() { return subject; }
- public String message() { return message; }
- public Optional<String> htmlMessage() { return htmlMessage; }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Mailer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Mailer.java
deleted file mode 100644
index e6095296d33..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/Mailer.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-
-/**
- * Allows sending e-mail from a particular <code>user@domain</code>.
- *
- * @author jonmv
- */
-public interface Mailer {
-
- /** Sends the given mail as the configured user@domain. */
- void send(Mail mail);
-
- /** Returns the user this is configured to use. */
- String user();
-
- /** Returns the domain this is configured to use. */
- String domain();
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MailerException.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MailerException.java
deleted file mode 100644
index 15d9cb8c5d9..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MailerException.java
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-/**
- * MailerException wrap all possible Mailer implementation exceptions
- *
- * @author enygaard
- */
-public class MailerException extends RuntimeException {
-
- public MailerException(Throwable ex) {
- super(ex);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockContactRetriever.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockContactRetriever.java
deleted file mode 100644
index 1336eef22be..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockContactRetriever.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-
-import java.net.URI;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Supplier;
-
-/**
- * @author olaa
- */
-public class MockContactRetriever implements ContactRetriever{
-
- private final Map<PropertyId, Supplier<Contact>> contacts = new HashMap<>();
-
- @Override
- public Contact getContact(Optional<PropertyId> propertyId) {
- return contacts.getOrDefault(propertyId.get(), this::contact).get();
- }
-
- public void addContact(PropertyId propertyId, Supplier<Contact> contact) {
- contacts.put(propertyId, contact);
- }
-
- public void addContact(PropertyId propertyId, Contact contact) {
- contacts.put(propertyId, () -> contact);
- }
-
- public Contact contact() {
- return new Contact(URI.create("contacts.tld"), URI.create("properties.tld"), URI.create("issues.tld"), Collections.emptyList(), "queue", Optional.of("component"));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockIssueHandler.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockIssueHandler.java
deleted file mode 100644
index 65709e050c5..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/MockIssueHandler.java
+++ /dev/null
@@ -1,194 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueInfo.Status;
-
-import java.io.InputStream;
-import java.net.URI;
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-/**
- * @author jvenstad
- */
-public class MockIssueHandler implements IssueHandler {
-
- private final Clock clock;
- private final AtomicLong counter = new AtomicLong();
- private final Map<IssueId, MockIssue> issues = new HashMap<>();
- private final Map<IssueId, Map<String, InputStream>> attachments = new HashMap<>();
- private final Map<String, ProjectInfo> projects = new HashMap<>();
-
- @Inject
- @SuppressWarnings("unused")
- public MockIssueHandler() {
- this(Clock.systemUTC());
- }
-
- public MockIssueHandler(Clock clock) {
- this.clock = clock;
- }
-
- @Override
- public IssueId file(Issue issue) {
- if (issue.assignee().isEmpty() && issue.assigneeId().isEmpty()) throw new RuntimeException();
- IssueId issueId = IssueId.from("" + counter.incrementAndGet());
- issues.put(issueId, new MockIssue(issue));
- return issueId;
- }
-
- @Override
- public List<IssueInfo> findAllBySimilarity(Issue issue) {
- return issues.entrySet().stream()
- .filter(entry -> entry.getValue().issue.summary().equals(issue.summary()))
- .map(entry -> new IssueInfo(entry.getKey(),
- entry.getValue().updated,
- entry.getValue().isOpen() ? Status.toDo : Status.done,
- entry.getValue().assigneeId))
- .toList();
- }
-
- @Override
- public void update(IssueId issueId, String description) {
- touch(issueId);
- }
-
- @Override
- public void commentOn(IssueId issueId, String comment) {
- touch(issueId);
- }
-
- @Override
- public boolean isOpen(IssueId issueId) {
- return issues.get(issueId).open;
- }
-
- @Override
- public boolean isActive(IssueId issueId, Duration maxInactivity) {
- return issues.get(issueId).updated.isAfter(clock.instant().minus(maxInactivity));
- }
-
- @Override
- public Optional<User> assigneeOf(IssueId issueId) {
- return Optional.ofNullable(issues.get(issueId).assignee);
- }
-
- @Override
- public Optional<AccountId> assigneeIdOf(IssueId issueId) {
- return Optional.ofNullable(issues.get(issueId).assigneeId);
- }
-
- @Override
- public boolean reassign(IssueId issueId, User assignee) {
- issues.get(issueId).assignee = assignee;
- touch(issueId);
- return true;
- }
-
- @Override
- public boolean addWatcher(IssueId issueId, String watcher) {
- issues.get(issueId).addWatcher(watcher);
- return true;
- }
-
- @Override
- public Optional<User> escalate(IssueId issueId, Contact contact) {
- List<List<User>> contacts = getContactUsers(contact);
- Optional<User> assignee = assigneeOf(issueId);
- int assigneeLevel = -1;
- if (assignee.isPresent())
- for (int level = contacts.size(); --level > assigneeLevel; )
- if (contacts.get(level).contains(assignee.get()))
- assigneeLevel = level;
-
- for (int level = assigneeLevel + 1; level < contacts.size(); level++)
- for (User target : contacts.get(level))
- if (reassign(issueId, target))
- return Optional.of(target);
-
- return Optional.empty();
- }
-
- @Override
- public boolean issueExists(Issue issue) {
- return issues.values().stream().anyMatch(i -> i.issue.summary().equals(issue.summary()));
- }
-
- @Override
- public ProjectInfo projectInfo(String projectKey) {
- return projects.get(projectKey);
- }
-
- @Override
- public void addAttachment(IssueId id, String filename, Supplier<InputStream> contentAsStream) {
- attachments.computeIfAbsent(id, __ -> new HashMap<>()).put(filename, contentAsStream.get());
- }
-
- public MockIssueHandler close(IssueId issueId) {
- issues.get(issueId).open = false;
- touch(issueId);
- return this;
- }
-
- public Map<IssueId, MockIssue> issues() {
- return issues;
- }
-
- private List<List<User>> getContactUsers(Contact contact) {
- return contact.persons().stream()
- .map(userList ->
- userList.stream().map(user ->
- user.split(" ")[0])
- .map(User::from)
- .toList()
- ).toList();
- }
-
-
- private void touch(IssueId issueId) {
- issues.get(issueId).updated = clock.instant();
- }
-
- public void addProject(String projectKey, ProjectInfo projectInfo) {
- projects.put(projectKey, projectInfo);
- }
-
- public class MockIssue {
-
- private Issue issue;
- private Instant updated;
- private boolean open;
- private User assignee;
- private AccountId assigneeId;
- private List<String> watchers;
-
- private MockIssue(Issue issue) {
- this.issue = issue;
- this.updated = clock.instant();
- this.open = true;
- this.assignee = issue.assignee().orElse(null);
- this.assigneeId = issue.assigneeId().orElse(null);
- this.watchers = new ArrayList<>();
- }
-
- public Issue issue() { return issue; }
- public User assignee() { return assignee; }
- public AccountId assigneeId() { return assigneeId; }
- public boolean isOpen() { return open; }
- public List<String> watchers() { return List.copyOf(watchers); }
- public void addWatcher(String watcher) { watchers.add(watcher); }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/OwnershipIssues.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/OwnershipIssues.java
deleted file mode 100644
index b4390a3b7d4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/OwnershipIssues.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import java.util.Optional;
-
-/**
- * Periodically issues ownership confirmation requests for given applications, and escalates the issues if needed.
- *
- * Even machines wrought from cold steel occasionally require the gentle touch only a fleshling can provide.
- * By making humans regularly acknowledge their dedication to given applications, this class provides the machine
- * with reassurance that any misbehaving applications will swiftly be dealt with.
- * Ignored confirmation requests are periodically redirected to humans of higher rank, until they are acknowledged.
- *
- * @author jonmv
- */
-public interface OwnershipIssues {
-
- /**
- * Ensure ownership of the given application has been recently confirmed by the given user.
- *
- * @param issueId ID of the previous ownership issue filed for the given application.
- * @param summary Summary of an application for which to file an issue.
- * @param assigneeId Issue assignee id
- * @param assignee Issue assignee
- * @param contact Contact info for the application tenant
- * @return ID of the created issue, if one was created.
- */
- Optional<IssueId> confirmOwnership(Optional<IssueId> issueId, ApplicationSummary summary, AccountId assigneeId, User assignee, Contact contact);
-
- /**
- * Make sure the given ownership confirmation request is acted upon, unless it is already acknowledged.
- * @param issueId ID of the ownership issue to escalate.
- * @param contact Contact information of application tenant
- */
- void ensureResponse(IssueId issueId, Optional<Contact> contact);
-
- /**
- * Get the owner of an application, given its ownership issue ID.
- * @param issueId ID of the ownership issue.
- * @return The owner of the application, if it has been confirmed.
- */
- Optional<AccountId> getConfirmedOwner(IssueId issueId);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ProjectInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ProjectInfo.java
deleted file mode 100644
index a3202a9c9c2..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/ProjectInfo.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import java.util.Map;
-
-/**
- * @author jvenstad
- * @author mortent
- */
-public class ProjectInfo {
-
- private final String id;
- private final Map<String, String> componentIds;
-
- public ProjectInfo(String id, Map<String, String> componentIds) {
- this.id = id;
- this.componentIds = componentIds;
- }
-
- public boolean hasComponent(String component) {
- return componentIds.containsKey(component);
- }
-
- public String id() {
- return id;
- }
-
- public Map<String, String> componentIds() {
- return componentIds;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/SystemMonitor.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/SystemMonitor.java
deleted file mode 100644
index 9033f12c5bf..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/SystemMonitor.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.component.Version;
-
-/**
- * Monitors a Vespa controller and its system.
- *
- * @author jonmv
- */
-public interface SystemMonitor {
-
- /** Notifies the monitor of the current system version and its confidence. */
- void reportSystemVersion(Version systemVersion, Confidence confidence);
-
- enum Confidence {
- aborted, broken, low, legacy, normal, high;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/User.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/User.java
deleted file mode 100644
index fe37bb178b4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/User.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import java.util.Objects;
-
-/**
- * Represents a human computer user, typically by UNIX account name.
- *
- * @author jonmv
- */
-public class User {
-
- private final String username;
-
- protected User(String username) {
- this.username = username;
- }
-
- public String username() {
- return username;
- }
-
- public String displayName() {
- return username;
- }
-
- public static User from(String username) {
- if (username.isEmpty())
- throw new IllegalArgumentException("Username may not be empty!");
-
- return new User(username);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if ( ! (o instanceof User)) return false;
- User that = (User) o;
- return Objects.equals(username, that.username);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(username);
- }
-
- @Override
- public String toString() {
- return username();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/package-info.java
deleted file mode 100644
index 34db709ade0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/organization/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.organization;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/package-info.java
deleted file mode 100644
index 856a3331d30..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/repair/RepairTicketReport.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/repair/RepairTicketReport.java
deleted file mode 100644
index e187f23e26d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/repair/RepairTicketReport.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.repair;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
-import static com.yahoo.yolean.Exceptions.uncheck;
-
-/**
- * @author olaa
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class RepairTicketReport {
-
- private static final String REPORT_ID = "repairTicket";
- private static final ObjectMapper objectMapper = new ObjectMapper();
-
- public String status;
- public String ticketNumber;
- public long createdMillis;
- public long updatedMillis;
-
- public RepairTicketReport(@JsonProperty("status") String status,
- @JsonProperty("ticketNumber") String ticketNumber,
- @JsonProperty("createdMillis") long createdMillis,
- @JsonProperty("updatedMillis") long updatedMillis) {
- this.status = status;
- this.ticketNumber = ticketNumber;
- this.createdMillis = createdMillis;
- this.updatedMillis = updatedMillis;
- }
-
- public String getStatus() {
- return status;
- }
-
- public String getTicketNumber() {
- return ticketNumber;
- }
-
- public long getCreatedMillis() {
- return createdMillis;
- }
-
- public long getUpdatedMillis() {
- return updatedMillis;
- }
-
- public static String getReportId() {
- return REPORT_ID;
- }
-
- public static RepairTicketReport fromJsonNode(String jsonReport) {
- return uncheck(() -> objectMapper.readValue(jsonReport, RepairTicketReport.class));
- }
-
- public JsonNode toJsonNode() {
- return uncheck(() -> objectMapper.valueToTree(this));
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/repair/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/repair/package-info.java
deleted file mode 100644
index 22659cef05a..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/repair/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.repair;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostInfo.java
deleted file mode 100644
index 0dcfb2d9823..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostInfo.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.resource;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.math.BigDecimal;
-
-/**
- * @author olaa
- */
-public class CostInfo {
-
- private final ApplicationId applicationId;
- private final ZoneId zoneId;
- private final BigDecimal cpuHours;
- private final BigDecimal memoryHours;
- private final BigDecimal diskHours;
- private final BigDecimal gpuHours;
- private final BigDecimal cpuCost;
- private final BigDecimal memoryCost;
- private final BigDecimal diskCost;
- private final BigDecimal gpuCost;
- private final NodeResources.Architecture architecture;
- private final int majorVersion;
- private final CloudAccount cloudAccount;
-
-
- public CostInfo(ApplicationId applicationId, ZoneId zoneId,
- BigDecimal cpuHours, BigDecimal memoryHours, BigDecimal diskHours, BigDecimal gpuHours,
- BigDecimal cpuCost, BigDecimal memoryCost, BigDecimal diskCost, BigDecimal gpuCost, NodeResources.Architecture architecture,
- int majorVersion, CloudAccount cloudAccount)
- {
- this.applicationId = applicationId;
- this.zoneId = zoneId;
- this.cpuHours = cpuHours;
- this.memoryHours = memoryHours;
- this.diskHours = diskHours;
- this.gpuHours = gpuHours;
- this.cpuCost = cpuCost;
- this.memoryCost = memoryCost;
- this.diskCost = diskCost;
- this.gpuCost = gpuCost;
- this.architecture = architecture;
- this.majorVersion = majorVersion;
- this.cloudAccount = cloudAccount;
- }
-
- public ApplicationId getApplicationId() {
- return applicationId;
- }
-
- public ZoneId getZoneId() {
- return zoneId;
- }
-
- public BigDecimal getCpuHours() {
- return cpuHours;
- }
-
- public BigDecimal getMemoryHours() {
- return memoryHours;
- }
-
- public BigDecimal getDiskHours() {
- return diskHours;
- }
-
- public BigDecimal getGpuHours() {
- return gpuHours;
- }
-
- public BigDecimal getCpuCost() {
- return cpuCost;
- }
-
- public BigDecimal getMemoryCost() {
- return memoryCost;
- }
-
- public BigDecimal getDiskCost() {
- return diskCost;
- }
-
- public BigDecimal getGpuCost() {
- return gpuCost;
- }
-
- public BigDecimal getTotalCost() {
- return cpuCost.add(memoryCost).add(diskCost).add(gpuCost);
- }
-
- public NodeResources.Architecture getArchitecture() {
- return architecture;
- }
-
- public int getMajorVersion() {
- return majorVersion;
- }
-
- public CloudAccount getCloudAccount() {
- return cloudAccount;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostReportConsumer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostReportConsumer.java
deleted file mode 100644
index 8de327fdd88..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostReportConsumer.java
+++ /dev/null
@@ -1,17 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.resource;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-
-import java.util.Map;
-
-/**
- * @author ldalves
- */
-public interface CostReportConsumer {
-
- void consume(String csv);
-
- Map<Property, ResourceAllocation> fixedAllocations();
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostReportConsumerMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostReportConsumerMock.java
deleted file mode 100644
index 2c15d23d03f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/CostReportConsumerMock.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.resource;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-
-import java.util.Map;
-import java.util.function.Consumer;
-
-/**
- * @author ldalves
- */
-public class CostReportConsumerMock implements CostReportConsumer {
-
- private final Consumer<String> csvConsumer;
- private final Map<Property, ResourceAllocation> fixedAllocations;
-
- public CostReportConsumerMock() {
- this.csvConsumer = (ignored) -> {};
- this.fixedAllocations = Map.of();
- }
-
- public CostReportConsumerMock(Consumer<String> csvConsumer, Map<Property, ResourceAllocation> fixedAllocations) {
- this.csvConsumer = csvConsumer;
- this.fixedAllocations = Map.copyOf(fixedAllocations);
- }
-
- @Override
- public void consume(String csv) {
- csvConsumer.accept(csv);
- }
-
- @Override
- public Map<Property, ResourceAllocation> fixedAllocations() {
- return fixedAllocations;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceAllocation.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceAllocation.java
deleted file mode 100644
index 3443d9b2f3f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceAllocation.java
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.resource;
-
-import com.yahoo.config.provision.NodeResources;
-
-import java.util.Objects;
-
-/**
- * An allocation of node resources.
- *
- * @author ldalves
- */
-public class ResourceAllocation {
-
- public static final ResourceAllocation ZERO = new ResourceAllocation(0, 0, 0, NodeResources.Architecture.getDefault());
-
- private final double cpuCores;
- private final double memoryGb;
- private final double diskGb;
- private final NodeResources.Architecture architecture;
-
- public ResourceAllocation(double cpuCores, double memoryGb, double diskGb, NodeResources.Architecture architecture) {
- this.cpuCores = cpuCores;
- this.memoryGb = memoryGb;
- this.diskGb = diskGb;
- this.architecture = architecture;
- }
-
- public double usageFraction(ResourceAllocation total) {
- return (cpuCores / total.cpuCores + memoryGb / total.memoryGb + diskGb / total.diskGb) / 3;
- }
-
- public double getCpuCores() {
- return cpuCores;
- }
-
- public double getMemoryGb() {
- return memoryGb;
- }
-
- public double getDiskGb() {
- return diskGb;
- }
-
- public NodeResources.Architecture getArchitecture() {
- return architecture;
- }
-
- /** Returns a copy of this with the given allocation added */
- public ResourceAllocation plus(ResourceAllocation allocation) {
- return new ResourceAllocation(cpuCores + allocation.cpuCores, memoryGb + allocation.memoryGb, diskGb + allocation.diskGb, architecture);
- }
-
- /** Returns a copy of this with each resource multiplied by given factor */
- public ResourceAllocation multiply(double multiplicand) {
- return new ResourceAllocation(cpuCores * multiplicand, memoryGb * multiplicand, diskGb * multiplicand, architecture);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof ResourceAllocation)) return false;
-
- ResourceAllocation other = (ResourceAllocation) o;
- return Double.compare(this.cpuCores, other.cpuCores) == 0 &&
- Double.compare(this.memoryGb, other.memoryGb) == 0 &&
- Double.compare(this.diskGb, other.diskGb) == 0;
-
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(cpuCores, memoryGb, diskGb);
- }
-
-}
-
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java
deleted file mode 100644
index 38c6ed767fa..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClient.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.resource;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-
-
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.YearMonth;
-import java.time.temporal.ChronoUnit;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * @author olaa
- */
-public interface ResourceDatabaseClient {
-
- void writeResourceSnapshots(Collection<ResourceSnapshot> snapshots);
-
- List<ResourceUsage> getResourceSnapshotsForPeriod(TenantName tenantName, long startTimestamp, long endTimestamp);
-
- void refreshMaterializedView();
-
- void writeScalingEvents(ClusterId clusterId, Collection<Cluster.ScalingEvent> scalingEvents);
-
- Map<ClusterId, List<Cluster.ScalingEvent>> scalingEvents(Instant from, Instant to, DeploymentId deploymentId);
-
- Set<YearMonth> getMonthsWithSnapshotsForTenant(TenantName tenantName);
-
- List<ResourceSnapshot> getRawSnapshotHistoryForTenant(TenantName tenantName, YearMonth yearMonth);
-
- Set<TenantName> getTenants();
-
- Instant getOldestSnapshotTimestamp(Set<DeploymentId> deployments);
-
- default List<ResourceUsage> getResourceSnapshotsForMonth(TenantName tenantName, YearMonth month) {
- return getResourceSnapshotsForPeriod(tenantName, getMonthStartTimeStamp(month), getMonthEndTimeStamp(month));
- }
-
- private long getMonthStartTimeStamp(YearMonth month) {
- LocalDate startOfMonth = LocalDate.of(month.getYear(), month.getMonth(), 1);
- return startOfMonth.atStartOfDay(java.time.ZoneId.of("UTC"))
- .toInstant()
- .toEpochMilli();
- }
- private long getMonthEndTimeStamp(YearMonth month) {
- LocalDate startOfMonth = LocalDate.of(month.getYear(), month.getMonth(), 1);
- return startOfMonth.plus(1, ChronoUnit.MONTHS)
- .atStartOfDay(java.time.ZoneId.of("UTC"))
- .toInstant()
- .toEpochMilli();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java
deleted file mode 100644
index 81dfdf4656c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceDatabaseClientMock.java
+++ /dev/null
@@ -1,162 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.resource;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
-
-import java.math.BigDecimal;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.YearMonth;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-/**
- * @author olaa
- */
-public class ResourceDatabaseClientMock implements ResourceDatabaseClient {
-
- PlanRegistry planRegistry;
- Map<TenantName, Plan> planMap = new HashMap<>();
- List<ResourceSnapshot> resourceSnapshots = new ArrayList<>();
- Map<ClusterId, List<Cluster.ScalingEvent>> scalingEvents = new HashMap<>();
- private boolean hasRefreshedMaterializedView = false;
-
- public ResourceDatabaseClientMock(PlanRegistry planRegistry) {
- this.planRegistry = planRegistry;
- }
-
- @Override
- public void writeResourceSnapshots(Collection<ResourceSnapshot> items) {
- this.resourceSnapshots.addAll(items);
- }
-
- @Override
- public Set<YearMonth> getMonthsWithSnapshotsForTenant(TenantName tenantName) {
- return Collections.emptySet();
- }
-
- @Override
- public List<ResourceSnapshot> getRawSnapshotHistoryForTenant(TenantName tenantName, YearMonth yearMonth) {
- return resourceSnapshots;
- }
-
- @Override
- public Set<TenantName> getTenants() {
- return resourceSnapshots.stream()
- .map(snapshot -> snapshot.getApplicationId().tenant())
- .collect(Collectors.toSet());
- }
-
- private List<ResourceUsage> resourceUsageFromSnapshots(Plan plan, List<ResourceSnapshot> snapshots) {
- snapshots.sort(Comparator.comparing(ResourceSnapshot::getTimestamp));
-
- return IntStream.range(0, snapshots.size())
- .mapToObj(idx -> {
- var a = snapshots.get(idx);
- var b = (idx + 1) < snapshots.size() ? snapshots.get(idx + 1) : null;
- var start = a.getTimestamp();
- var end = Optional.ofNullable(b).map(ResourceSnapshot::getTimestamp).orElse(start.plusSeconds(120));
- var d = BigDecimal.valueOf(Duration.between(start, end).toMillis());
- return new ResourceUsage(
- a.getApplicationId(),
- a.getZoneId(),
- plan,
- a.resources().architecture(),
- a.getMajorVersion(),
- a.getAccount(),
- BigDecimal.valueOf(a.resources().vcpu()).multiply(d),
- BigDecimal.valueOf(a.resources().memoryGb()).multiply(d),
- BigDecimal.valueOf(a.resources().diskGb()).multiply(d),
- BigDecimal.valueOf(a.resources().gpuResources().count() * a.resources().gpuResources().memoryGb()).multiply(d));
- })
- .toList();
- }
-
- private ResourceUsage resourceUsageAdd(ResourceUsage a, ResourceUsage b) {
- assert a.getApplicationId().equals(b.getApplicationId());
- assert a.getZoneId().equals(b.getZoneId());
- assert a.getPlan().equals(b.getPlan());
- assert a.getArchitecture().equals(b.getArchitecture());
- assert a.getCloudAccount().equals(b.getCloudAccount());
- return new ResourceUsage(
- a.getApplicationId(),
- a.getZoneId(),
- a.getPlan(),
- a.getArchitecture(),
- a.getMajorVersion(),
- a.getCloudAccount(),
- a.getCpuMillis().add(b.getCpuMillis()),
- a.getMemoryMillis().add(b.getMemoryMillis()),
- a.getDiskMillis().add(b.getDiskMillis()),
- a.getGpuMillis().add(b.getGpuMillis()));
- }
-
- @Override
- public List<ResourceUsage> getResourceSnapshotsForPeriod(TenantName tenantName, long start, long end) {
- var tenantPlan = planMap.getOrDefault(tenantName, planRegistry.defaultPlan());
-
- return resourceSnapshots.stream()
- .filter(snapshot -> snapshot.getTimestamp().isAfter(Instant.ofEpochMilli(start)))
- .filter(snapshot -> snapshot.getTimestamp().isBefore(Instant.ofEpochMilli(end)))
- .filter(snapshot -> snapshot.getApplicationId().tenant().equals(tenantName))
- .collect(Collectors.groupingBy(
- usage -> Objects.hash(usage.getApplicationId(), usage.getZoneId(), tenantPlan.id().value(), usage.resources().architecture(), usage.getMajorVersion())
- ))
- .values().stream()
- .map(snapshots -> resourceUsageFromSnapshots(tenantPlan, snapshots))
- .map(usages -> usages.stream().reduce(this::resourceUsageAdd))
- .filter(Optional::isPresent)
- .map(Optional::get)
- .toList();
- }
-
- @Override
- public void refreshMaterializedView() {
- hasRefreshedMaterializedView = true;
- }
-
- @Override
- public Instant getOldestSnapshotTimestamp(Set<DeploymentId> deployments) {
- return Instant.ofEpochMilli(987654L);
- }
-
- @Override
- public void writeScalingEvents(ClusterId clusterId, Collection<Cluster.ScalingEvent> scalingEvents) {
- this.scalingEvents.put(clusterId, List.copyOf(scalingEvents));
- }
-
- @Override
- public Map<ClusterId, List<Cluster.ScalingEvent>> scalingEvents(Instant from, Instant to, DeploymentId deploymentId) {
- return Map.of();
- }
-
- public void setPlan(TenantName tenant, Plan plan) {
- planMap.put(tenant, plan);
- }
-
- public boolean hasRefreshedMaterializedView() {
- return hasRefreshedMaterializedView;
- }
-
- public List<ResourceSnapshot> resourceSnapshots() {
- return resourceSnapshots;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java
deleted file mode 100644
index dc14a043183..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceSnapshot.java
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.resource;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-
-import java.time.Instant;
-import java.util.Collection;
-import java.util.List;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * Represents the resources allocated to a deployment at a specific point in time.
- *
- * @author olaa
- */
-public class ResourceSnapshot {
-
- private static final NodeResources zero = new NodeResources(
- 0, 0, 0, 0,
- NodeResources.DiskSpeed.any,
- NodeResources.StorageType.any,
- NodeResources.Architecture.any,
- NodeResources.GpuResources.zero());
-
- private final ApplicationId applicationId;
- private final NodeResources resources;
- private final Instant timestamp;
- private final ZoneId zoneId;
- private final int majorVersion;
- private final CloudAccount account;
-
- public ResourceSnapshot(ApplicationId applicationId, NodeResources resources, Instant timestamp, ZoneId zoneId, int majorVersion, CloudAccount account) {
- this.applicationId = applicationId;
- this.resources = resources;
- this.timestamp = timestamp;
- this.zoneId = zoneId;
- this.majorVersion = majorVersion;
- this.account = account;
- }
-
- public static ResourceSnapshot from(ApplicationId applicationId, int nodes, NodeResources resources, Instant timestamp, ZoneId zoneId) {
- return new ResourceSnapshot(applicationId, resources.multipliedBy(nodes), timestamp, zoneId, 0, CloudAccount.empty);
- }
-
- public static ResourceSnapshot from(List<Node> nodes, Instant timestamp, ZoneId zoneId) {
- var application = exactlyOne("application", nodes.stream()
- .filter(node -> node.owner().isPresent())
- .map(node -> node.owner().get())
- .collect(Collectors.toSet()));
-
- var version = exactlyOne("version", nodes.stream()
- .map(n -> n.wantedVersion().getMajor())
- .collect(Collectors.toSet()));
-
- var account = exactlyOne("account", nodes.stream()
- .map(Node::cloudAccount)
- .collect(Collectors.toSet()));
-
- var resources = nodes.stream()
- .map(Node::resources)
- .reduce(zero, ResourceSnapshot::addResources);
-
- return new ResourceSnapshot(application, resources, timestamp, zoneId, version, account);
- }
-
- public ApplicationId getApplicationId() {
- return applicationId;
- }
-
- public NodeResources resources() {
- return resources;
- }
-
- public Instant getTimestamp() {
- return timestamp;
- }
-
- public ZoneId getZoneId() {
- return zoneId;
- }
-
- public int getMajorVersion() {
- return majorVersion;
- }
-
- public CloudAccount getAccount() {
- return account;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof ResourceSnapshot)) return false;
-
- ResourceSnapshot other = (ResourceSnapshot) o;
- return this.applicationId.equals(other.applicationId) &&
- this.resources.equals(other.resources) &&
- this.timestamp.equals(other.timestamp) &&
- this.zoneId.equals(other.zoneId) &&
- this.majorVersion == other.majorVersion;
- }
-
- @Override
- public int hashCode(){
- return Objects.hash(applicationId, resources, timestamp, zoneId, majorVersion);
- }
-
- /* This function does pretty much the same thing as NodeResources::add, but it allows adding resources
- * where some dimensions that are not relevant for billing (yet) are not the same.
- *
- * TODO: Make this code respect all dimensions.
- */
- private static NodeResources addResources(NodeResources a, NodeResources b) {
- if (a.architecture() != b.architecture() && a.architecture() != NodeResources.Architecture.any && b.architecture() != NodeResources.Architecture.any) {
- throw new IllegalArgumentException(a + " and " + b + " are not interchangeable for resource snapshots");
- }
- return new NodeResources(
- a.vcpu() + b.vcpu(),
- a.memoryGb() + b.memoryGb(),
- a.diskGb() + b.diskGb(),
- 0,
- NodeResources.DiskSpeed.any,
- NodeResources.StorageType.any,
- a.architecture() == NodeResources.Architecture.any ? b.architecture() : a.architecture(),
- a.gpuResources().plus(b.gpuResources()));
- }
-
- private static <T> T exactlyOne(String resource, Collection<T> collection) {
- if (collection.size() != 1) throw new IllegalArgumentException("More than one '" + resource + "', was: " + collection.size());
- return collection.iterator().next();
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceUsage.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceUsage.java
deleted file mode 100644
index 3cb611af8a0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/ResourceUsage.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.resource;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
-
-import java.math.BigDecimal;
-
-public class ResourceUsage {
-
- private final ApplicationId applicationId;
- private final ZoneId zoneId;
- private final Plan plan;
- private final BigDecimal cpuMillis;
- private final BigDecimal memoryMillis;
- private final BigDecimal diskMillis;
- private final BigDecimal gpuMillis;
- private final NodeResources.Architecture architecture;
- private final int majorVersion;
- private final CloudAccount cloudAccount;
-
- public ResourceUsage(ApplicationId applicationId, ZoneId zoneId, Plan plan, NodeResources.Architecture architecture,
- int majorVersion, CloudAccount cloudAccount, BigDecimal cpuMillis, BigDecimal memoryMillis, BigDecimal diskMillis, BigDecimal gpuMillis)
- {
- this.applicationId = applicationId;
- this.zoneId = zoneId;
- this.cpuMillis = cpuMillis;
- this.memoryMillis = memoryMillis;
- this.diskMillis = diskMillis;
- this.gpuMillis = gpuMillis;
- this.plan = plan;
- this.architecture = architecture;
- this.majorVersion = majorVersion;
- this.cloudAccount = cloudAccount;
- }
-
- public ApplicationId getApplicationId() {
- return applicationId;
- }
-
- public ZoneId getZoneId() {
- return zoneId;
- }
-
- public BigDecimal getCpuMillis() {
- return cpuMillis;
- }
-
- public BigDecimal getMemoryMillis() {
- return memoryMillis;
- }
-
- public BigDecimal getDiskMillis() {
- return diskMillis;
- }
-
- public BigDecimal getGpuMillis() { return gpuMillis; }
-
- public Plan getPlan() {
- return plan;
- }
-
- public int getMajorVersion() {
- return majorVersion;
- }
-
- public NodeResources.Architecture getArchitecture() {
- return architecture;
- }
-
- public CloudAccount getCloudAccount(){
- return cloudAccount;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/package-info.java
deleted file mode 100644
index 127fcc4e5d3..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/resource/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.resource;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/routing/RotationStatus.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/routing/RotationStatus.java
deleted file mode 100644
index 973592dfd33..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/routing/RotationStatus.java
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.routing;
-
-/**
- * Represents the health status of a global rotation.
- *
- * @author andreer
- */
-public enum RotationStatus {
- IN, OUT, UNKNOWN
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/routing/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/routing/package-info.java
deleted file mode 100644
index 1534a0bd4a4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/routing/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.routing;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/EndpointSecretManager.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/EndpointSecretManager.java
deleted file mode 100644
index bc51502f397..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/EndpointSecretManager.java
+++ /dev/null
@@ -1,6 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.secrets;
-
-public interface EndpointSecretManager {
- void deleteSecret(String secretName);
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/GcpSecretStore.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/GcpSecretStore.java
deleted file mode 100644
index eeb7f7b06e5..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/GcpSecretStore.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.secrets;
-
-import java.time.Duration;
-import java.util.Optional;
-
-public interface GcpSecretStore {
-
- void setSecret(String secretName, Optional<Duration> expiry, String... accessorServiceAccounts);
-
- void addSecretVersion(String secretName, String secretValue);
-
- String getLatestSecretVersion(String secretName);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopEndpointSecretManager.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopEndpointSecretManager.java
deleted file mode 100644
index d7952635994..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopEndpointSecretManager.java
+++ /dev/null
@@ -1,9 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.secrets;
-
-public class NoopEndpointSecretManager implements EndpointSecretManager {
- @Override
- public void deleteSecret(String secretName) {
- // noop
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopGcpSecretStore.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopGcpSecretStore.java
deleted file mode 100644
index 5dc98c41600..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopGcpSecretStore.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.secrets;
-
-import java.time.Duration;
-import java.util.Optional;
-
-/**
- * @author olaa
- */
-public class NoopGcpSecretStore implements GcpSecretStore {
-
- @Override
- public void setSecret(String secretName, Optional<Duration> expiry, String... accessorServiceAccounts) { }
-
- @Override
- public void addSecretVersion(String secretName, String secretValue) { }
-
- @Override
- public String getLatestSecretVersion(String secretName) { return ""; }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopTenantSecretService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopTenantSecretService.java
deleted file mode 100644
index a039c67ae9d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/NoopTenantSecretService.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.secrets;
-
-import com.yahoo.config.provision.TenantName;
-
-import java.util.List;
-
-/**
- * @author olaa
- */
-public class NoopTenantSecretService implements TenantSecretService {
-
- @Override
- public void addSecretStore(TenantName tenant, TenantSecretStore tenantSecretStore, String externalId) {}
-
- @Override
- public void deleteSecretStore(TenantName tenant, TenantSecretStore tenantSecretStore) {}
-
- @Override
- public void cleanupSecretStores(List<TenantName> deletedTenants) {}
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/TenantSecretService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/TenantSecretService.java
deleted file mode 100644
index 1f0d44f58e5..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/TenantSecretService.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.secrets;
-
-import com.yahoo.config.provision.TenantName;
-
-import java.util.List;
-
-/**
- * @author olaa
- */
-public interface TenantSecretService {
-
- void addSecretStore(TenantName tenant, TenantSecretStore tenantSecretStore, String externalId);
-
- void deleteSecretStore(TenantName tenant, TenantSecretStore tenantSecretStore);
-
- void cleanupSecretStores(List<TenantName> deletedTenants);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/TenantSecretStore.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/TenantSecretStore.java
deleted file mode 100644
index 37f33ac2d51..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/TenantSecretStore.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.secrets;
-
-import java.util.Objects;
-
-/**
- * @author olaa
- */
-public class TenantSecretStore {
-
- private final String name;
- private final String awsId;
- private final String role;
-
- public TenantSecretStore(String name, String awsId, String role) {
- this.name = name;
- this.awsId = awsId;
- this.role = role;
- }
-
- public String getName() {
- return name;
- }
-
- public String getAwsId() {
- return awsId;
- }
-
- public String getRole() {
- return role;
- }
-
- public boolean isValid() {
- return !name.isBlank() &&
- !awsId.isBlank() &&
- !role.isBlank();
- }
-
- @Override
- public String toString() {
- return "TenantSecretStore{" +
- "name='" + name + '\'' +
- ", awsId='" + awsId + '\'' +
- ", role='" + role + '\'' +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TenantSecretStore that = (TenantSecretStore) o;
- return name.equals(that.name) &&
- awsId.equals(that.awsId) &&
- role.equals(that.role);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name, awsId, role);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/package-info.java
deleted file mode 100644
index c7fc927a54c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/secrets/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.secrets;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/DummyOwnershipIssues.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/DummyOwnershipIssues.java
deleted file mode 100644
index ef30781dab0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/DummyOwnershipIssues.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.vespa.hosted.controller.api.integration.organization.AccountId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.ApplicationSummary;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-
-import java.util.Optional;
-
-/**
- * @author jonmv
- */
-public class DummyOwnershipIssues implements OwnershipIssues {
-
- @Override
- public Optional<IssueId> confirmOwnership(Optional<IssueId> issueId, ApplicationSummary summary, AccountId assigneeId, User assignee, Contact contact) {
- return Optional.empty();
- }
-
- @Override
- public void ensureResponse(IssueId issueId, Optional<Contact> contact) {
- }
-
- @Override
- public Optional<AccountId> getConfirmedOwner(IssueId issueId) {
- return Optional.empty();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/DummySystemMonitor.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/DummySystemMonitor.java
deleted file mode 100644
index 297ca77b509..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/DummySystemMonitor.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.component.Version;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor;
-
-/**
- * @author jonmv
- */
-public class DummySystemMonitor implements SystemMonitor {
-
- @Override
- public void reportSystemVersion(Version systemVersion, Confidence confidence) { }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingDeploymentIssues.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingDeploymentIssues.java
deleted file mode 100644
index 6015da03763..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/LoggingDeploymentIssues.java
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.AccountId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentIssues;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.logging.Logger;
-
-/**
- * A memory backed implementation of the Issues API which logs changes and does nothing else.
- *
- * @author bratseth
- * @author jonmv
- */
-public class LoggingDeploymentIssues implements DeploymentIssues {
-
- private static final Logger log = Logger.getLogger(LoggingDeploymentIssues.class.getName());
-
- /** Whether the platform is currently broken. */
- protected final AtomicBoolean platformIssue = new AtomicBoolean(false);
- /** Last updates for each issue -- used to determine if issues are already logged and when to escalate. */
- protected final Map<IssueId, Instant> issueUpdates = new HashMap<>();
-
- /** Used to fabricate unique issue ids. */
- private final AtomicLong issueIdSequence = new AtomicLong(0);
-
- private final Clock clock;
-
- @SuppressWarnings("unused") // Created by dependency injection.
- @Inject
- public LoggingDeploymentIssues() {
- this(Clock.systemUTC());
- }
-
- protected LoggingDeploymentIssues(Clock clock) {
- this.clock = clock;
- }
-
- @Override
- public IssueId fileUnlessOpen(Optional<IssueId> issueId, ApplicationId applicationId, AccountId assigneeId, User assignee, Contact contact) {
- return fileUnlessPresent(issueId, applicationId);
- }
-
- @Override
- public IssueId fileUnlessOpen(Collection<ApplicationId> applicationIds, Version version) {
- if ( ! platformIssue.get())
- log.info("These applications are all failing deployment to version " + version + ":\n" + applicationIds);
-
- platformIssue.set(true);
- return null;
- }
-
- @Override
- public void escalateIfInactive(IssueId issueId, Duration maxInactivity, Optional<Contact> contact) {
- if (issueUpdates.containsKey(issueId) && issueUpdates.get(issueId).isBefore(clock.instant().minus(maxInactivity)))
- escalateIssue(issueId);
- }
-
- protected void escalateIssue(IssueId issueId) {
- issueUpdates.put(issueId, clock.instant());
- log.info("Deployment issue " + issueId + " should be escalated.");
- }
-
- protected IssueId fileIssue(ApplicationId applicationId) {
- IssueId issueId = IssueId.from("" + issueIdSequence.incrementAndGet());
- issueUpdates.put(issueId, clock.instant());
- log.info("Deployment issue " + issueId +": " + applicationId + " has failing deployments.");
- return issueId;
- }
-
- private IssueId fileUnlessPresent(Optional<IssueId> issueId, ApplicationId applicationId) {
- platformIssue.set(false);
- return issueId.filter(issueUpdates::containsKey).orElseGet(() -> fileIssue(applicationId));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMailer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMailer.java
deleted file mode 100644
index d63782c79de..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMailer.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer;
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class MockMailer implements Mailer {
-
- public final Map<String, List<Mail>> mails = new HashMap<>();
- public final boolean blackhole;
-
- public MockMailer() {
- this(false);
- }
-
- MockMailer(boolean blackhole) {
- this.blackhole = blackhole;
- }
-
- public static MockMailer blackhole() {
- return new MockMailer(true);
- }
-
- @Override
- public void send(Mail mail) {
- if (blackhole) {
- return;
- }
- for (String recipient : mail.recipients()) {
- mails.putIfAbsent(recipient, new ArrayList<>());
- mails.get(recipient).add(mail);
- }
- }
-
- @Override
- public String user() {
- return "user";
- }
-
- @Override
- public String domain() {
- return "domain";
- }
-
- /** Returns the list of mails sent to the given recipient. Modifications affect the set of mails stored in this. */
- public List<Mail> inbox(String recipient) {
- return mails.getOrDefault(recipient, List.of());
- }
-
- public void reset() {
- mails.clear();
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMavenRepository.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMavenRepository.java
deleted file mode 100644
index 4bd71b817e7..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockMavenRepository.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.component.Version;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.vespa.hosted.controller.api.integration.maven.ArtifactId;
-import com.yahoo.vespa.hosted.controller.api.integration.maven.MavenRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.maven.Metadata;
-
-import java.time.Clock;
-import java.time.Instant;
-import java.time.ZoneId;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicReference;
-
-/**
- * Mock repository for maven artifacts, that returns a static metadata.
- *
- * @author jonmv
- */
-public class MockMavenRepository implements MavenRepository {
-
- public static final ArtifactId id = new ArtifactId("ai.vespa", "search");
-
- private final Clock clock;
- private AtomicReference<Instant> lastUpdated;
- private final List<Version> versions = new ArrayList<>();
-
- @Inject
- public MockMavenRepository() {
- this(Clock.fixed(Instant.EPOCH, ZoneId.of("UTC")));
- }
-
- public MockMavenRepository(Clock clock) {
- this.clock = clock;
- this.lastUpdated = new AtomicReference<>(clock.instant().minusSeconds(10801));
- versions.addAll(List.of(Version.fromString("6.0"),
- Version.fromString("6.1"),
- Version.fromString("6.2")));
- }
-
- public void addVersion(Version version) {
- lastUpdated.set(clock.instant());
- versions.add(version);
- }
-
- @Override
- public Metadata metadata() {
- return new Metadata(id, lastUpdated.get(), versions);
- }
-
- @Override
- public ArtifactId artifactId() {
- return id;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockRunDataStore.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockRunDataStore.java
deleted file mode 100644
index e02a9e4da42..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockRunDataStore.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.integration.RunDataStore;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
-
-import static com.yahoo.yolean.Exceptions.uncheck;
-
-/**
- * @author jonmv
- */
-public class MockRunDataStore implements RunDataStore {
-
- private final Map<RunId, byte[]> logs = new ConcurrentHashMap<>();
- private final Map<RunId, byte[]> reports = new ConcurrentHashMap<>();
- private final Map<RunId, byte[]> vespaLogs = new ConcurrentHashMap<>();
- private final Map<RunId, byte[]> testerLogs = new ConcurrentHashMap<>();
-
- @Override
- public Optional<byte[]> get(RunId id) {
- return Optional.ofNullable(logs.get(id));
- }
-
- @Override
- public void put(RunId id, byte[] log) {
- logs.put(id, log);
- }
-
- @Override
- public Optional<byte[]> getTestReport(RunId id) {
- return Optional.ofNullable(reports.get(id));
- }
-
- @Override
- public void putTestReport(RunId id, byte[] report) {
- reports.put(id, report);
- }
-
- @Override
- public void delete(RunId id) {
- logs.remove(id);
- reports.remove(id);
- }
-
- @Override
- public void delete(ApplicationId id) {
- logs.keySet().removeIf(runId -> runId.application().equals(id));
- reports.keySet().removeIf(runId -> runId.application().equals(id));
- }
-
- @Override
- public void putLogs(RunId id, boolean tester, InputStream logs) {
- (tester ? testerLogs : vespaLogs).put(id, uncheck(logs::readAllBytes));
- }
-
- @Override
- public InputStream getLogs(RunId id, boolean tester) {
- return new ByteArrayInputStream((tester ? testerLogs : vespaLogs).get(id));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockTesterCloud.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockTesterCloud.java
deleted file mode 100644
index 97d6248ec8d..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockTesterCloud.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import ai.vespa.http.DomainName;
-import com.google.common.net.InetAddresses;
-import com.yahoo.config.provision.EndpointsChecker;
-import com.yahoo.config.provision.EndpointsChecker.Endpoint;
-import com.yahoo.config.provision.EndpointsChecker.Availability;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-
-import java.net.InetAddress;
-import java.net.URI;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Status.NOT_STARTED;
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Status.RUNNING;
-
-public class MockTesterCloud implements TesterCloud {
-
- private final NameService nameService;
- private final EndpointsChecker endpointsChecker = EndpointsChecker.mock(this::resolveHostName, this::resolveCname, __ -> Availability.ready);
-
- private List<LogEntry> log = new ArrayList<>();
- private Status status = NOT_STARTED;
- private byte[] config;
- private Optional<TestReport> testReport = Optional.empty();
-
- public MockTesterCloud(NameService nameService) {
- this.nameService = nameService;
- }
-
- @Override
- public void startTests(DeploymentId deploymentId, Suite suite, byte[] config) {
- this.status = RUNNING;
- this.config = config;
- }
-
- @Override
- public List<LogEntry> getLog(DeploymentId deploymentId, long after) {
- return log.stream().filter(entry -> entry.id() > after).toList();
- }
-
- @Override
- public Status getStatus(DeploymentId deploymentId) { return status; }
-
- @Override
- public boolean testerReady(DeploymentId deploymentId) {
- return true;
- }
-
- @Override
- public Availability verifyEndpoints(DeploymentId deploymentId, List<Endpoint> endpoints, boolean initialDeployment) {
- return endpointsChecker.endpointsAvailable(endpoints);
- }
-
- private Optional<InetAddress> resolveHostName(DomainName hostname) {
- return nameService.findRecords(Record.Type.A, RecordName.from(hostname.value())).stream()
- .findFirst()
- .map(record -> InetAddresses.forString(record.data().asString()))
- .or(() -> Optional.of(InetAddresses.forString("1.2.3.4")));
- }
-
- private Optional<DomainName> resolveCname(DomainName hostName) {
- return nameService.findRecords(Record.Type.CNAME, RecordName.from(hostName.value())).stream()
- .findFirst()
- .map(record -> DomainName.of(record.data().asString().substring(0, record.data().asString().length() - 1)));
- }
-
- @Override
- public Optional<TestReport> getTestReport(DeploymentId deploymentId) {
- return testReport;
- }
-
- public void testReport(TestReport testReport) {
- this.testReport = Optional.ofNullable(testReport);
- }
-
- public void add(LogEntry entry) {
- log.add(entry);
- }
-
- public void clearLog() {
- log.clear();
- }
-
- public void set(Status status) {
- this.status = status;
- }
-
- public byte[] config() {
- return config;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockUserManagement.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockUserManagement.java
deleted file mode 100644
index e7e1a78ed3e..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/MockUserManagement.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.jdisc.http.filter.security.misc.User;
-import com.yahoo.vespa.hosted.controller.api.integration.user.UserId;
-import com.yahoo.vespa.hosted.controller.api.integration.user.UserManagement;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * @author jonmv
- */
-public class MockUserManagement extends AbstractComponent implements UserManagement {
-
- private final Map<Role, Set<User>> memberships = new HashMap<>();
-
- private Set<User> get(Role role) {
- var membership = memberships.get(role);
- if (membership == null) {
- throw new IllegalArgumentException(role + " not found");
- }
- return membership;
- }
-
- @Override
- public void createRole(Role role) {
- if (memberships.containsKey(role))
- throw new IllegalArgumentException(role + " already exists.");
-
- memberships.put(role, new HashSet<>());
- }
-
- @Override
- public void deleteRole(Role role) {
- memberships.remove(role);
- }
-
- @Override
- public void addUsers(Role role, Collection<UserId> users) {
- List<User> userObjs = users.stream()
- .map(id -> new User(id.value(), id.value(), null, null))
- .toList();
- get(role).addAll(userObjs);
- }
-
- @Override
- public void addToRoles(UserId user, Collection<Role> roles) {
- for (Role role : roles) {
- addUsers(role, Collections.singletonList(user));
- }
- }
-
- @Override
- public void removeUsers(Role role, Collection<UserId> users) {
- get(role).removeIf(user -> users.contains(new UserId(user.email())));
- }
-
- @Override
- public void removeFromRoles(UserId user, Collection<Role> roles) {
- for (Role role : roles) {
- removeUsers(role, Collections.singletonList(user));
- }
- }
-
- @Override
- public List<User> listUsers(Role role) {
- return List.copyOf(get(role));
- }
-
- @Override
- public List<Role> listRoles(UserId userId) {
- return memberships.entrySet().stream()
- .filter(entry -> entry.getValue().stream().anyMatch(user -> user.name().equals(userId.value())))
- .map(Map.Entry::getKey)
- .toList();
- }
-
- @Override
- public List<Role> listRoles() {
- return new ArrayList<>(memberships.keySet());
- }
-
- @Override
- public Optional<User> findUser(String email) {
- return memberships.values().stream()
- .flatMap(Collection::stream)
- .filter(user -> user.email().equals(email))
- .findFirst();
- }
-
- @Override
- public List<User> findUsers(String query) {
- return List.of();
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/package-info.java
deleted file mode 100644
index 18148246da3..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/stubs/package-info.java
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * No-dependency implementations of integration interfaces for setups where we want to avoid contacting
- * certain thirds-party systems.
- *
- * @author bratseth
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.stubs;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleMaintainer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleMaintainer.java
deleted file mode 100644
index 7b36bbd0824..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleMaintainer.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.user;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.util.List;
-
-/**
- * @author olaa
- */
-public interface RoleMaintainer {
-
- /** Given the set of all existing tenants and applications, delete any superflous roles */
- void deleteLeftoverRoles(List<Tenant> tenants, List<ApplicationId> applications);
-
- /** Finds the subset of tenants that should be deleted based on role/domain existence */
- List<Tenant> tenantsToDelete(List<Tenant> tenants);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleMaintainerMock.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleMaintainerMock.java
deleted file mode 100644
index daac3ee41ea..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/RoleMaintainerMock.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.user;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * @author olaa
- */
-public class RoleMaintainerMock implements RoleMaintainer {
-
- private List<Tenant> tenantsToDelete = new ArrayList<>();
-
- @Override
- public void deleteLeftoverRoles(List<Tenant> tenants, List<ApplicationId> applications) {
-
- }
-
- @Override
- public List<Tenant> tenantsToDelete(List<Tenant> tenants) {
- return tenantsToDelete;
- }
-
- public void mockTenantToDelete(Tenant tenant) {
- tenantsToDelete.add(tenant);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/Roles.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/Roles.java
deleted file mode 100644
index beb01653434..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/Roles.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.user;
-
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.role.ApplicationRole;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
-import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
-
-import java.util.List;
-
-/**
- * Validation, utility and serialization methods for roles used in user management.
- *
- * @author jonmv
- */
-public class Roles {
-
- private Roles() { }
-
- /** Returns the list of {@link TenantRole}s a {@link UserId} may be a member of. */
- public static List<TenantRole> tenantRoles(TenantName tenant) {
- return List.of(Role.administrator(tenant),
- Role.developer(tenant),
- Role.reader(tenant));
- }
-
- /** Returns the list of {@link ApplicationRole}s a {@link UserId} may be a member of. */
- public static List<ApplicationRole> applicationRoles(TenantName tenant, ApplicationName application) {
- return List.of();
- }
-
- /** Returns the {@link Role} the given value represents. */
- public static Role toRole(String value) {
- String[] parts = value.split("\\.");
- if (parts.length == 1 && parts[0].equals("hostedOperator")) return Role.hostedOperator();
- if (parts.length == 1 && parts[0].equals("hostedSupporter")) return Role.hostedSupporter();
- if (parts.length == 1 && parts[0].equals("hostedAccountant")) return Role.hostedAccountant();
- if (parts.length == 2) return toRole(TenantName.from(parts[0]), parts[1]);
- if (parts.length == 3) return toRole(TenantName.from(parts[0]), ApplicationName.from(parts[1]), parts[2]);
- throw new IllegalArgumentException("Malformed or illegal role value '" + value + "'.");
- }
-
- /** Returns the {@link Role} the given tenant, application and role names correspond to. */
- public static Role toRole(TenantName tenant, String roleName) {
- switch (roleName) {
- case "administrator": return Role.administrator(tenant);
- case "developer": return Role.developer(tenant);
- case "reader": return Role.reader(tenant);
- default: throw new IllegalArgumentException("Malformed or illegal role name '" + roleName + "'.");
- }
- }
-
- /** Returns the {@link Role} the given tenant and role names correspond to. */
- public static Role toRole(TenantName tenant, ApplicationName application, String roleName) {
- switch (roleName) {
- case "headless": return Role.headless(tenant, application);
- default: throw new IllegalArgumentException("Malformed or illegal role name '" + roleName + "'.");
- }
- }
-
- /** Returns a serialised representation the given role. */
- public static String valueOf(Role role) {
- if (role instanceof TenantRole) return valueOf((TenantRole) role);
- if (role instanceof ApplicationRole) return valueOf((ApplicationRole) role);
- throw new IllegalArgumentException("Unexpected role type '" + role.getClass().getName() + "'.");
- }
-
- private static String valueOf(TenantRole role) {
- return valueOf(role.tenant()) + "." + valueOf(role.definition());
- }
-
- private static String valueOf(ApplicationRole role) {
- return valueOf(role.tenant()) + "." + valueOf(role.application()) + "." + valueOf(role.definition());
- }
-
- private static String valueOf(TenantName tenant) {
- if (tenant.value().contains("."))
- throw new IllegalArgumentException("Tenant names may not contain '.'.");
-
- return tenant.value();
- }
-
- private static String valueOf(ApplicationName application) {
- if (application.value().contains("."))
- throw new IllegalArgumentException("Application names may not contain '.'.");
-
- return application.value();
- }
-
- private static String valueOf(RoleDefinition role) {
- switch (role) {
- case administrator: return "administrator";
- case developer: return "developer";
- case reader: return "reader";
- case headless: return "headless";
- default: throw new IllegalArgumentException("No value defined for role '" + role + "'.");
- }
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserId.java
deleted file mode 100644
index 4bf74e482c6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserId.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.user;
-
-import java.util.Objects;
-
-/**
- * An identifier for a user.
- *
- * @author jonmv
- */
-public class UserId {
-
- private final String value;
-
- public UserId(String value) {
- if (value.isBlank())
- throw new IllegalArgumentException("Id must be non-blank.");
- this.value = value;
- }
-
- public String value() { return value; }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- UserId id = (UserId) o;
- return Objects.equals(value, id.value);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(value);
- }
-
- @Override
- public String toString() {
- return "user '" + value + "'";
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserManagement.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserManagement.java
deleted file mode 100644
index 7853175c215..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserManagement.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.user;
-
-import com.yahoo.jdisc.http.filter.security.misc.User;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * Management of {@link UserId}s as members of {@link Role}s.
- *
- * @author jonmv
- */
-public interface UserManagement {
-
- /** Creates the given role, or throws if the role already exists. */
- void createRole(Role role);
-
- /** Ensures the given role does not exist. */
- void deleteRole(Role role);
-
- /** Ensures the given users exist, and are part of the given role, or throws if the role does not exist. */
- void addUsers(Role role, Collection<UserId> users);
-
- /** Ensures the given user exist, and are part of the given roles, or throws if the roles does not exist. */
- void addToRoles(UserId user, Collection<Role> roles);
-
- /** Ensures none of the given users are part of the given role, or throws if the role does not exist. */
- void removeUsers(Role role, Collection<UserId> users);
-
- /** Ensures the given users are not part of the given role, or throws if the roles does not exist. */
- void removeFromRoles(UserId user, Collection<Role> roles);
-
- /** Returns all users in the given role, or throws if the role does not exist. */
- List<User> listUsers(Role role);
-
- /** Returns all roles of which the given user is part, or throws if the user does not exist */
- List<Role> listRoles(UserId user);
-
- /** Returns all roles */
- List<Role> listRoles();
-
- /** Find a user with all attributes */
- Optional<User> findUser(String email);
-
- /** Find all users from the database given query */
- List<User> findUsers(String query);
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserSessionManager.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserSessionManager.java
deleted file mode 100644
index 79071d5be9b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/UserSessionManager.java
+++ /dev/null
@@ -1,13 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.user;
-
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-
-/**
- * @author freva
- */
-public interface UserSessionManager {
-
- /** Returns whether the existing session for the given SecurityContext should be expired */
- boolean shouldExpireSessionFor(SecurityContext context);
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/package-info.java
deleted file mode 100644
index 081d17728f7..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/user/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.user;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequest.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequest.java
deleted file mode 100644
index 2516a3192fc..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequest.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.vcmr;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * @author olaa
- */
-public class ChangeRequest {
-
- private final String id;
- private final ChangeRequestSource changeRequestSource;
- private final List<String> impactedSwitches;
- private final List<String> impactedHosts;
- private final Approval approval;
- private final Impact impact;
-
- public ChangeRequest(String id, ChangeRequestSource changeRequestSource, List<String> impactedSwitches, List<String> impactedHosts, Approval approval, Impact impact) {
- this.id = Objects.requireNonNull(id);
- this.changeRequestSource = Objects.requireNonNull(changeRequestSource);
- this.impactedSwitches = Objects.requireNonNull(impactedSwitches);
- this.impactedHosts = Objects.requireNonNull(impactedHosts);
- this.approval = Objects.requireNonNull(approval);
- this.impact = Objects.requireNonNull(impact);
- }
-
- public String getId() {
- return id;
- }
-
- public ChangeRequestSource getChangeRequestSource() {
- return changeRequestSource;
- }
-
- public List<String> getImpactedSwitches() {
- return impactedSwitches;
- }
-
- public List<String> getImpactedHosts() {
- return impactedHosts;
- }
-
- public Approval getApproval() {
- return approval;
- }
-
- public Impact getImpact() {
- return impact;
- }
-
- @Override
- public String toString() {
- return "ChangeRequest{" +
- "id='" + id + '\'' +
- ", changeRequestSource=" + changeRequestSource +
- ", impactedSwitches=" + impactedSwitches +
- ", impactedHosts=" + impactedHosts +
- ", approval=" + approval +
- ", impact=" + impact +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ChangeRequest that = (ChangeRequest) o;
- return approval == that.approval &&
- Objects.equals(id, that.id) &&
- Objects.equals(changeRequestSource, that.changeRequestSource) &&
- Objects.equals(impactedSwitches, that.impactedSwitches) &&
- Objects.equals(impactedHosts, that.impactedHosts) &&
- impact == that.impact;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id, changeRequestSource, impactedSwitches, impactedHosts, approval, impact);
- }
-
- public static class Builder {
- private String id;
- private ChangeRequestSource changeRequestSource;
- private List<String> impactedSwitches;
- private List<String> impactedHosts;
- private Approval approval = Approval.OTHER;
- private Impact impact;
-
-
- public Builder id(String id) {
- this.id = id;
- return this;
- }
-
- public Builder changeRequestSource(ChangeRequestSource changeRequestSource) {
- this.changeRequestSource = changeRequestSource;
- return this;
- }
-
- public Builder impactedSwitches(List<String> impactedSwitches) {
- this.impactedSwitches = impactedSwitches;
- return this;
- }
-
- public Builder impactedHosts(List<String> impactedHosts) {
- this.impactedHosts = impactedHosts;
- return this;
- }
-
- public Builder approval(Approval approval) {
- this.approval = approval;
- return this;
- }
-
- public Builder impact(Impact impact) {
- this.impact = impact;
- return this;
- }
-
- public ChangeRequest build() {
- return new ChangeRequest(id, changeRequestSource, impactedSwitches, impactedHosts, approval, impact);
- }
-
- public String getId() {
- return this.id;
- }
- }
-
- public enum Impact {
- NONE,
- LOW,
- MODERATE,
- HIGH,
- VERY_HIGH,
- UNKNOWN
- }
-
- public enum Approval {
- REQUESTED,
- APPROVED,
- REJECTED,
- OTHER
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestClient.java
deleted file mode 100644
index 52138276cfa..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestClient.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.vcmr;
-
-import java.util.List;
-
-/**
- * @author olaa
- */
-public interface ChangeRequestClient {
-
- /** Get upcoming change requests and updated status of previously stored requests */
- List<ChangeRequest> getChangeRequests(List<ChangeRequest> changeRequests);
-
- void approveChangeRequest(ChangeRequest changeRequest);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestSource.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestSource.java
deleted file mode 100644
index ef7b67fb254..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/ChangeRequestSource.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.vcmr;
-
-import java.time.ZonedDateTime;
-import java.util.List;
-import java.util.Objects;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource.Status.*;
-
-/**
- * @author olaa
- */
-public record ChangeRequestSource(String system,
- String id,
- String url,
- Status status,
- ZonedDateTime plannedStartTime,
- ZonedDateTime plannedEndTime,
- String category) {
-
- public boolean isClosed() {
- return List.of(CLOSED, CANCELED, COMPLETE).contains(status);
- }
-
- public enum Status {
- DRAFT,
- WAITING_FOR_APPROVAL,
- APPROVED,
- STARTED,
- COMPLETE,
- CLOSED,
- CANCELED,
- UNKNOWN_STATUS
- }
-
- public static class Builder {
- private String system;
- private String id;
- private String url;
- private Status status;
- private ZonedDateTime plannedStartTime;
- private ZonedDateTime plannedEndTime;
- private String category;
-
- public Builder system(String system) {
- this.system = system;
- return this;
- }
-
- public Builder id(String id) {
- this.id = id;
- return this;
- }
-
- public Builder url(String url) {
- this.url = url;
- return this;
- }
-
- public Builder status(Status status) {
- this.status = status;
- return this;
- }
-
- public Builder plannedStartTime(ZonedDateTime plannedStartTime) {
- this.plannedStartTime = plannedStartTime;
- return this;
- }
-
- public Builder plannedEndTime(ZonedDateTime plannedEndTime) {
- this.plannedEndTime = plannedEndTime;
- return this;
- }
-
- public Builder category(String category) {
- this.category = category;
- return this;
- }
-
- public ChangeRequestSource build() {
- return new ChangeRequestSource(system, id, url, status, plannedStartTime, plannedEndTime, category);
- }
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/HostAction.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/HostAction.java
deleted file mode 100644
index 154565f0a3c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/HostAction.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.vcmr;
-
-import java.time.Instant;
-import java.util.Objects;
-
-/**
- * @author olaa
- *
- * Contains planned/current action for a host impacted by a change request
- */
-public class HostAction {
-
- private final String hostname;
- private final State state;
- private final Instant lastUpdated;
-
- public HostAction(String hostname, State state, Instant lastUpdated) {
- this.hostname = hostname;
- this.state = state;
- this.lastUpdated = lastUpdated;
- }
-
- public String getHostname() {
- return hostname;
- }
-
- public State getState() {
- return state;
- }
-
- public Instant getLastUpdated() {
- return lastUpdated;
- }
-
- public HostAction withState(State state) {
- return new HostAction(hostname, state, this.state == state ? lastUpdated : Instant.now());
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- HostAction that = (HostAction) o;
- return Objects.equals(hostname, that.hostname) &&
- state == that.state &&
- Objects.equals(lastUpdated, that.lastUpdated);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(hostname, state, lastUpdated);
- }
-
- @Override
- public String toString() {
- return "HostAction{" +
- "hostname='" + hostname + '\'' +
- ", state=" + state +
- ", lastUpdated=" + lastUpdated +
- '}';
- }
-
- public enum State {
- REQUIRES_OPERATOR_ACTION,
- PENDING_RETIREMENT,
- OUT_OF_SYNC,
- NONE,
- RETIRING,
- READY,
- RETIRED,
- COMPLETE
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/MockChangeRequestClient.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/MockChangeRequestClient.java
deleted file mode 100644
index 31f2dec80c0..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/MockChangeRequestClient.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.vcmr;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * @author olaa
- */
-public class MockChangeRequestClient implements ChangeRequestClient {
-
- private List<ChangeRequest> upcomingChangeRequests = new ArrayList<>();
- private List<ChangeRequest> approvedChangeRequests = new ArrayList<>();
-
- @Override
- public List<ChangeRequest> getChangeRequests(List<ChangeRequest> changeRequests) {
- return upcomingChangeRequests;
- }
-
- @Override
- public void approveChangeRequest(ChangeRequest changeRequest) {
- approvedChangeRequests.add(changeRequest);
- }
-
- public void setUpcomingChangeRequests(List<ChangeRequest> changeRequests) {
- upcomingChangeRequests = changeRequests;
- }
-
- public List<ChangeRequest> getApprovedChangeRequests() {
- return approvedChangeRequests;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VcmrReport.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VcmrReport.java
deleted file mode 100644
index beb45ba5428..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VcmrReport.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.vcmr;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-
-import java.time.ZonedDateTime;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
-import static com.yahoo.yolean.Exceptions.uncheck;
-
-/**
- *
- * Node repository report containing list of upcoming VCMRs impacting a node
- *
- * @author olaa
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-public class VcmrReport {
-
- private static final String REPORT_ID = "vcmr";
- private static final ObjectMapper objectMapper = new ObjectMapper()
- .registerModule(new JavaTimeModule());
-
- @JsonProperty("upcoming")
- private Set<Vcmr> vcmrs;
-
- public VcmrReport() {
- this(new HashSet<>());
- }
-
- public VcmrReport(Set<Vcmr> vcmrs) {
- this.vcmrs = vcmrs;
- }
-
- public Set<Vcmr> getVcmrs() {
- return vcmrs;
- }
-
- /**
- * @return true if list of VCMRs is changed
- */
- public boolean addVcmr(ChangeRequestSource source) {
- var vcmr = new Vcmr(source.id(), source.status().name(), source.plannedStartTime(), source.plannedEndTime(), source.category());
- if (vcmrs.contains(vcmr))
- return false;
-
- // Remove to catch any changes in start/end time
- removeVcmr(source.id());
- return vcmrs.add(vcmr);
- }
-
- public boolean removeVcmr(String id) {
- return vcmrs.removeIf(vcmr -> id.equals(vcmr.id()));
- }
-
- public static String getReportId() {
- return REPORT_ID;
- }
-
- /**
- * Serialization functions - mapped to {@link Node#reports()}
- */
- public static VcmrReport fromReports(Map<String, String> reports) {
- var serialized = reports.get(REPORT_ID);
- if (serialized == null)
- return new VcmrReport();
-
- return uncheck(() -> objectMapper.readValue(serialized, VcmrReport.class));
- }
-
- /**
- * Set report to 'null' if list is empty - clearing the report
- * See NodePatcher in node-repository
- */
- public Map<String, String> toNodeReports() {
- Map<String, String> reports = new HashMap<>();
- String json = vcmrs.isEmpty() ?
- null : uncheck(() -> objectMapper.valueToTree(this).toString());
- reports.put(REPORT_ID, json);
- return reports;
- }
-
- @Override
- public String toString() {
- return "VCMRReport{" + vcmrs + "}";
- }
-
- public record Vcmr (@JsonProperty("id") String id,
- @JsonProperty("status") String status,
- @JsonProperty("plannedStartTime") ZonedDateTime plannedStartTime,
- @JsonProperty("plannedEndTime") ZonedDateTime plannedEndTime,
- @JsonProperty("category") String category) {}
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VespaChangeRequest.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VespaChangeRequest.java
deleted file mode 100644
index c7e84d4f9c4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/VespaChangeRequest.java
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.vcmr;
-
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * @author olaa
- */
-public class VespaChangeRequest extends ChangeRequest {
-
- private final Status status;
- private final ZoneId zoneId;
- // TODO: Create applicationActionPlan
- private final List<HostAction> hostActionPlan;
-
- public VespaChangeRequest(String id, ChangeRequestSource changeRequestSource, List<String> impactedSwitches, List<String> impactedHosts, Approval approval, Impact impact, Status status, List<HostAction> hostActionPlan, ZoneId zoneId) {
- super(id, changeRequestSource, impactedSwitches, impactedHosts, approval, impact);
- this.status = status;
- this.hostActionPlan = hostActionPlan;
- this.zoneId = zoneId;
- }
- public VespaChangeRequest(ChangeRequest changeRequest, ZoneId zoneId) {
- this(changeRequest.getId(), changeRequest.getChangeRequestSource(), changeRequest.getImpactedSwitches(),
- changeRequest.getImpactedHosts(), changeRequest.getApproval(), changeRequest.getImpact(), Status.PENDING_ASSESSMENT, List.of(), zoneId);
- }
-
- public Status getStatus() {
- return status;
- }
-
- public List<HostAction> getHostActionPlan() {
- return hostActionPlan;
- }
-
- public ZoneId getZoneId() {
- return zoneId;
- }
-
- public VespaChangeRequest withStatus(Status status) {
- return new VespaChangeRequest(getId(), getChangeRequestSource(), getImpactedSwitches(), getImpactedHosts(), getApproval(), getImpact(), status, hostActionPlan, zoneId);
- }
-
- public VespaChangeRequest withSource(ChangeRequestSource source) {
- return new VespaChangeRequest(getId(), source, getImpactedSwitches(), getImpactedHosts(), getApproval(), getImpact(), status, hostActionPlan, zoneId);
- }
-
- public VespaChangeRequest withImpact(Impact impact) {
- return new VespaChangeRequest(getId(), getChangeRequestSource(), getImpactedSwitches(), getImpactedHosts(), getApproval(), impact, status, hostActionPlan, zoneId);
- }
-
- public VespaChangeRequest withApproval(Approval approval) {
- return new VespaChangeRequest(getId(), getChangeRequestSource(), getImpactedSwitches(), getImpactedHosts(), approval, getImpact(), status, hostActionPlan, zoneId);
- }
-
- public VespaChangeRequest withActionPlan(List<HostAction> hostActionPlan) {
- return new VespaChangeRequest(getId(), getChangeRequestSource(), getImpactedSwitches(), getImpactedHosts(), getApproval(), getImpact(), status, hostActionPlan, zoneId);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- if (!super.equals(o)) return false;
- VespaChangeRequest that = (VespaChangeRequest) o;
- return status == that.status &&
- Objects.equals(hostActionPlan, that.hostActionPlan) &&
- Objects.equals(zoneId, that.zoneId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(super.hashCode(), status, hostActionPlan, zoneId);
- }
-
- @Override
- public String toString() {
- return "VespaChangeRequest{" +
- "id='" + getId() + '\'' +
- ", changeRequestSource=" + getChangeRequestSource() +
- ", impactedSwitches=" + getImpactedSwitches() +
- ", impactedHosts=" + getImpactedHosts() +
- ", approval=" + getApproval() +
- ", impact=" + getImpact() +
- ", status=" + status +
- ", zoneId=" + zoneId +
- ", hostActionPlan=" + hostActionPlan +
- '}';
- }
-
- public enum Status {
- COMPLETED,
- READY,
- IN_PROGRESS,
- PENDING_ACTION,
- PENDING_ASSESSMENT,
- REQUIRES_OPERATOR_ACTION,
- OUT_OF_SYNC,
- NOOP
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/package-info.java
deleted file mode 100644
index e7eeab65ffb..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/vcmr/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.vcmr;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java
deleted file mode 100644
index 92c0a6b1fbb..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/ZoneRegistry.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.zone;
-
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.UpgradePolicy;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneFilter;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-
-import java.net.URI;
-import java.time.Duration;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Optional;
-
-/**
- * Provides information about zones in a hosted Vespa system, and about the system.
- *
- * @author mpolden
- */
-public interface ZoneRegistry {
-
- /** Returns whether the system of this registry contains the given zone */
- boolean hasZone(ZoneId zoneId);
-
- /** Returns whether cloudAccount in this system supports given zone */
- boolean hasZone(ZoneId zoneId, CloudAccount cloudAccount);
-
- /** Returns whether the given cloud account is not one of the system accounts */
- boolean isExternal(CloudAccount cloudAccount);
-
- default boolean isExclave(CloudAccount cloudAccount) {
- return system().isPublic() && isExternal(cloudAccount);
- }
-
- /** Returns a list containing the id of all zones in this registry */
- ZoneFilter zones();
-
- /** Returns a list containing the id of all zones in this registry, including the system. */
- ZoneFilter zonesIncludingSystem();
-
- /** Returns the default region for the given environment, if one is configured */
- Optional<RegionName> getDefaultRegion(Environment environment);
-
- /** Throws {@link NoSuchElementException} if there is no such zone (in the system). */
- ZoneApi get(ZoneId zoneId);
-
- /** Returns the URI for the config server VIP in the given zone */
- URI getConfigServerVipUri(ZoneId zoneId);
-
- /** Returns the VIP hostname for the shared routing layer in given zone, if any */
- Optional<String> getVipHostname(ZoneId zoneId);
-
- /** Returns the time to live for deployments in the given zone, or empty if this is infinite */
- Optional<Duration> getDeploymentTimeToLive(ZoneId zoneId);
-
- /** Returns the monitoring system URL for the given deployment */
- URI getMonitoringSystemUri(DeploymentId deploymentId);
-
- /** Returns the system of this registry */
- SystemName system();
-
- /** Returns the system of this registry as a zone */
- ZoneApi systemZone();
-
- /** Return the configserver's Athenz service identity */
- AthenzIdentity getConfigServerHttpsIdentity(ZoneId zoneId);
-
- /** Return the Athenz service identity for a given node type */
- AthenzIdentity getNodeAthenzIdentity(ZoneId zoneId, NodeType nodeType);
-
- /** Return the system Athenz domain */
- AthenzDomain accessControlDomain();
-
- /** Returns the Vespa upgrade policy to use for zones in this registry */
- UpgradePolicy upgradePolicy();
-
- /** Returns the OS upgrade policy to use for zones belonging to given cloud, in this registry */
- UpgradePolicy osUpgradePolicy(CloudName cloud);
-
- /** Returns all OS upgrade policies */
- List<UpgradePolicy> osUpgradePolicies();
-
- /** Returns the routing method used by given zone */
- RoutingMethod routingMethod(ZoneId zone);
-
- /** Returns a URL to the controller's api endpoint */
- URI apiUrl();
-
- /** IAM tenant developer role ARN */
- Optional<String> tenantDeveloperRoleArn(TenantName tenant);
-
- /** Returns athenz domain tied to the given cloud account, if any */
- Optional<AthenzDomain> cloudAccountAthenzDomain(CloudAccount cloudAccount);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/package-info.java
deleted file mode 100644
index 30949dff889..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/zone/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.integration.zone;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Action.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Action.java
deleted file mode 100644
index 45aefc0456a..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Action.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.jdisc.http.HttpRequest;
-
-import java.util.EnumSet;
-
-/**
- * Action defines an operation, typically a HTTP method, that may be performed on an entity in the controller
- * (e.g. tenant or application).
- *
- * @author mpolden
- */
-public enum Action {
-
- create,
- read,
- update,
- delete;
-
- static EnumSet<Action> all() {
- return EnumSet.allOf(Action.class);
- }
-
- static EnumSet<Action> write() {
- return EnumSet.of(create, update, delete);
- }
-
- /** Returns the appropriate action for given HTTP method */
- public static Action from(HttpRequest.Method method) {
- switch (method) {
- case POST: return Action.create;
- case GET:
- case OPTIONS:
- case HEAD: return Action.read;
- case PUT:
- case PATCH: return Action.update;
- case DELETE: return Action.delete;
- default: throw new IllegalArgumentException("No action defined for method " + method);
- }
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/ApplicationRole.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/ApplicationRole.java
deleted file mode 100644
index 1f6691bb04b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/ApplicationRole.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.TenantName;
-
-/**
- * A {@link Role} with a {@link Context} of a {@link TenantName} and an {@link ApplicationName}.
- *
- * @author jonmv
- */
-public class ApplicationRole extends Role {
-
- ApplicationRole(RoleDefinition roleDefinition, TenantName tenant, ApplicationName application) {
- super(roleDefinition, Context.limitedTo(tenant, application));
- }
-
- /** Returns the {@link TenantName} this is bound to. */
- public TenantName tenant() { return context.tenant().get(); }
-
- /** Returns the {@link ApplicationName} this is bound to. */
- public ApplicationName application() { return context.application().get(); }
-
- @Override
- public String toString() {
- return "role '" + definition() + "' of '" + application() + "' owned by '" + tenant() + "'";
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Context.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Context.java
deleted file mode 100644
index 699ed386bd8..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Context.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.TenantName;
-
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * The context in which a role is valid. This is immutable.
- *
- * @author mpolden
- */
-class Context {
-
- private final Optional<TenantName> tenant;
- private final Optional<ApplicationName> application;
- private final Optional<InstanceName> instance;
-
- private Context(Optional<TenantName> tenant, Optional<ApplicationName> application, Optional<InstanceName> instance) {
- this.tenant = Objects.requireNonNull(tenant, "tenant must be non-null");
- this.application = Objects.requireNonNull(application, "application must be non-null");
- this.instance = Objects.requireNonNull(instance, "instance must be non-null");
- }
-
- /** A specific tenant this is valid for, if any */
- Optional<TenantName> tenant() {
- return tenant;
- }
-
- /** A specific application this is valid for, if any */
- Optional<ApplicationName> application() {
- return application;
- }
-
- /** A specific instance this is valid for, if any */
- Optional<InstanceName> instance() {
- return instance;
- }
-
- /** Returns a context that has no restrictions on tenant or application */
- static Context unlimited() {
- return new Context(Optional.empty(), Optional.empty(), Optional.empty());
- }
-
- /** Returns a context that is limited to given tenant */
- static Context limitedTo(TenantName tenant) {
- return new Context(Optional.of(tenant), Optional.empty(), Optional.empty());
- }
-
- /** Returns a context that is limited to given tenant and application */
- static Context limitedTo(TenantName tenant, ApplicationName application) {
- return new Context(Optional.of(tenant), Optional.of(application), Optional.empty());
- }
-
- /** Returns a context that is limited to given tenant, application, and instance */
- static Context limitedTo(TenantName tenant, ApplicationName application, InstanceName instance) {
- return new Context(Optional.of(tenant), Optional.of(application), Optional.of(instance));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Context context = (Context) o;
- return tenant.equals(context.tenant) &&
- application.equals(context.application) &&
- instance.equals(context.instance);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(tenant, application, instance);
- }
-
- @Override
- public String toString() {
- return "tenant " + tenant.map(TenantName::value).orElse("[none]") + ", "
- + "application " + application.map(ApplicationName::value).orElse("[none]") + ", "
- + "instance " + instance.map(InstanceName::value).orElse("[none]");
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Enforcer.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Enforcer.java
deleted file mode 100644
index 8b7d0d0b4fb..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Enforcer.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.config.provision.SystemName;
-
-import java.net.URI;
-import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * Checks whether {@link Role}s have the required {@link Privilege}s to perform {@link Action}s on given {@link java.net.URI}s.
- *
- * @author jonmv
- */
-public class Enforcer {
-
- private static final Logger logger = Logger.getLogger(Enforcer.class.getName());
-
- private final SystemName system;
- public Enforcer(SystemName system) {
- this.system = system;
- }
-
- /** Returns whether {@code role} has permission to perform {@code action} on {@code resource}, in this enforcer's system. */
- public boolean allows(Role role, Action action, URI resource) {
- List<Policy> matchingPolicies = role.definition().policies().stream()
- .filter(policy -> policy.evaluate(action, resource, role.context, system))
- .toList();
- logger.log(Level.FINE, "Matching policies for " +
- "role: " + role.definition().name() + ", "+
- "action " + action.name() + ", " +
- resource.getPath() + " : " +
- matchingPolicies.stream().map(Enum::name).collect(Collectors.joining()));
- return !matchingPolicies.isEmpty();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
deleted file mode 100644
index 9a3ea71660b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/PathGroup.java
+++ /dev/null
@@ -1,354 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.restapi.Path;
-
-import java.net.URI;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * This declares and groups all known REST API paths in the controller.
- *
- * When creating a new API, its paths must be added here and a policy must be declared in {@link Policy}.
- *
- * @author mpolden
- * @author jonmv
- */
-enum PathGroup {
-
- /** Paths exclusive to operators (including read), used for system management. */
- classifiedOperator("/application/v4/notifications",
- "/routing/v1/",
- "/routing/v1/status/environment/{*}",
- "/routing/v1/inactive/environment/{*}",
- "/configserver/v1/{*}",
- "/deployment/v1/{*}"),
-
- /** Paths used for system management by operators. */
- operator("/cores/v1/{*}",
- "/controller/v1/{*}",
- "/flags/v1/{*}",
- "/loadbalancers/v1/{*}",
- "/nodes/v2/{*}",
- "/orchestrator/v1/{*}",
- "/os/v1/{*}",
- "/provision/v2/{*}",
- "/zone/v2/{*}",
- "/state/v1/{*}",
- "/changemanagement/v1/{*}",
- "/application/v4/search/{*}"),
-
- /** Paths used for creating and reading user resources. */
- user("/application/v4/user",
- "/athenz/v1/{*}"),
-
- /** Paths used for creating tenants with proper access control. */
- tenant(Matcher.tenant,
- "/application/v4/tenant/{tenant}"),
-
- /** Paths used for user management on the tenant level. */
- tenantUsers(Matcher.tenant,
- "/user/v1/tenant/{tenant}"),
-
- /** Paths used by tenant administrators. */
- tenantInfo(Matcher.tenant,
- "/application/v4/tenant/{tenant}/application/",
- "/application/v4/tenant/{tenant}/info/",
- "/application/v4/tenant/{tenant}/info/profile",
- "/application/v4/tenant/{tenant}/info/billing",
- "/application/v4/tenant/{tenant}/info/contacts",
- "/application/v4/tenant/{tenant}/info/resend-mail-verification",
- "/application/v4/tenant/{tenant}/notifications",
- "/routing/v1/status/tenant/{tenant}/{*}"),
-
- tenantKeys(Matcher.tenant,
- "/application/v4/tenant/{tenant}/key/"),
-
- tenantArchiveAccess(Matcher.tenant,
- "/application/v4/tenant/{tenant}/archive-access",
- "/application/v4/tenant/{tenant}/archive-access/aws",
- "/application/v4/tenant/{tenant}/archive-access/gcp"),
-
- billing(Matcher.tenant,
- "/billing/v2/tenant/{tenant}/{*}"),
-
- billingAux("/billing/v2/countries"),
-
- accountant("/billing/v2/accountant/{*}"),
-
- userSearch("/user/v1/find"),
-
- applicationKeys(Matcher.tenant,
- Matcher.application,
- "/application/v4/tenant/{tenant}/application/{application}/key/"),
-
- /** Path for the base application resource. */
- application(Matcher.tenant,
- Matcher.application,
- "/application/v4/tenant/{tenant}/application/{application}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}"),
-
- /** Paths used for user management on the application level. */
- applicationUsers(Matcher.tenant,
- Matcher.application,
- "/user/v1/tenant/{tenant}/application/{application}"),
-
- /** Paths used by application administrators. */
- applicationInfo(Matcher.tenant,
- Matcher.application,
- "/application/v4/tenant/{tenant}/application/{application}/submit/{build}",
- "/application/v4/tenant/{tenant}/application/{application}/package",
- "/application/v4/tenant/{tenant}/application/{application}/diff/{number}",
- "/application/v4/tenant/{tenant}/application/{application}/compile-version",
- "/application/v4/tenant/{tenant}/application/{application}/deployment",
- "/application/v4/tenant/{tenant}/application/{application}/deploying/{*}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/deploying/{*}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/job/{*}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/nodes",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/clusters",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/content/{*}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/logs",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/orchestrator",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/private-services",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/suspended",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/service/{*}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/access/support",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/global-rotation/{*}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/scaling",
- "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/nodes",
- "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/clusters",
- "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/logs",
- "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/metrics",
- "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/suspended",
- "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/service/{*}",
- "/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{ignored}/global-rotation/{*}",
- "/application/v4/tenant/{tenant}/application/{application}/metering"),
-
- applicationRouting(Matcher.tenant,
- Matcher.application, "/routing/v1/inactive/tenant/{tenant}/application/{application}/instance/{ignored}/environment/prod/region/{region}"),
-
- // TODO jonmv: remove
- /** Path used to restart development nodes. */
- developmentRestart(Matcher.tenant,
- Matcher.application,
- Matcher.instance,
- "/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/dev/region/{region}/restart",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/perf/region/{region}/restart",
- "/application/v4/tenant/{tenant}/application/{application}/environment/dev/region/{region}/instance/{instance}/restart",
- "/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}/restart"),
-
- // TODO jonmv: remove
- /** Path used to restart production nodes. */
- productionRestart(Matcher.tenant,
- Matcher.application,
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/prod/region/{region}/restart",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/test/region/{region}/restart",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/staging/region/{region}/restart",
- "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{ignored}/restart",
- "/application/v4/tenant/{tenant}/application/{application}/environment/test/region/{region}/instance/{ignored}/restart",
- "/application/v4/tenant/{tenant}/application/{application}/environment/staging/region/{region}/instance/{ignored}/restart"),
-
- /** Path used to manipulate reindexing status. */
- reindexing(Matcher.tenant,
- Matcher.application,
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/reindex",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/reindexing"),
-
- serviceDump(Matcher.tenant,
- Matcher.application,
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/node/{node}/service-dump"),
-
- dropDocuments(Matcher.tenant,
- Matcher.application,
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/{environment}/region/{region}/drop-documents"),
-
- /** Paths used for development deployments. */
- developmentDeployment(Matcher.tenant,
- Matcher.application,
- Matcher.instance,
- "/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploy/{job}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/dev/region/{region}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/dev/region/{region}/deploy",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/dev/region/{region}/suspend",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/perf/region/{region}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/perf/region/{region}/deploy",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/perf/region/{region}/suspend",
- "/application/v4/tenant/{tenant}/application/{application}/environment/dev/region/{region}/instance/{instance}",
- "/application/v4/tenant/{tenant}/application/{application}/environment/dev/region/{region}/instance/{instance}/deploy",
- "/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}",
- "/application/v4/tenant/{tenant}/application/{application}/environment/perf/region/{region}/instance/{instance}/deploy"),
-
- // TODO jonmv: remove
- /** Paths used for production deployments. */
- productionDeployment(Matcher.tenant,
- Matcher.application,
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/prod/region/{region}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/prod/region/{region}/deploy",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/test/region/{region}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/test/region/{region}/deploy",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/staging/region/{region}",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/environment/staging/region/{region}/deploy",
- "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{ignored}",
- "/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{ignored}/deploy",
- "/application/v4/tenant/{tenant}/application/{application}/environment/test/region/{region}/instance/{ignored}",
- "/application/v4/tenant/{tenant}/application/{application}/environment/test/region/{region}/instance/{ignored}/deploy",
- "/application/v4/tenant/{tenant}/application/{application}/environment/staging/region/{region}/instance/{ignored}",
- "/application/v4/tenant/{tenant}/application/{application}/environment/staging/region/{region}/instance/{ignored}/deploy"),
-
- /** Paths used for continuous deployment to production. */
- submission(Matcher.tenant,
- Matcher.application,
- "/application/v4/tenant/{tenant}/application/{application}/submit",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/submit"),
-
- /** Paths used for other tasks by build services. */ // TODO: This will vanish.
- buildService(Matcher.tenant,
- Matcher.application,
- "/application/v4/tenant/{tenant}/application/{application}/jobreport",
- "/application/v4/tenant/{tenant}/application/{application}/instance/{ignored}/jobreport"),
-
- /** Paths which contain (not very strictly) classified information about customers. */
- classifiedTenantInfo("/application/v4/",
- "/application/v4/tenant/"),
-
- /** Paths providing public information. */
- publicInfo("/user/v1/user", // Information about who you are.
- "/badge/v1/{*}", // Badges for deployment jobs.
- "/zone/v1/{*}", // Lists environment and regions.
- "/cli/v1/{*}", // Public information for Vespa CLI.
- "/pricing/v1/{*}", // Pricing information
- "/.well-known/{*}"),
-
- /** Paths used for deploying system-wide feature flags. */
- systemFlagsDeploy("/system-flags/v1/deploy"),
-
-
- /** Paths used for "dry-running" system-wide feature flags. */
- systemFlagsDryrun("/system-flags/v1/dryrun"),
-
- /** Paths used for receiving payment callbacks */
- paymentProcessor("/payment/notification"),
-
- /** Path used for listing endpoint certificate request and re-requesting endpoint certificates */
- endpointCertificates("/endpointcertificates/"),
-
- /** Path used for secret store management */
- secretStore(Matcher.tenant, "/application/v4/tenant/{tenant}/secret-store/{*}"),
-
- /** Paths used to proxy Horizon metric requests */
- horizonProxy("/horizon/v1/{*}"),
-
- /** Paths used to list and request access to tenant resources */
- accessRequests(Matcher.tenant, "/application/v4/tenant/{tenant}/access/request/operator"),
-
- /** Paths used to approve requests to access tenant resources */
- accessRequestApproval(Matcher.tenant, "/application/v4/tenant/{tenant}/access/approve/operator",
- "/application/v4/tenant/{tenant}/access/managed/operator"),
-
- /** Path used for email verification */
- emailVerification("/user/v1/email/verify"),
-
- /** Path used for dataplane token */
- dataplaneToken(Matcher.tenant,"/application/v4/tenant/{tenant}/token", "/application/v4/tenant/{tenant}/token/{ignored}"),
-
- termsOfService(Matcher.tenant, "/application/v4/tenant/{tenant}/terms-of-service");
-
- final List<String> pathSpecs;
- final List<Matcher> matchers;
-
- PathGroup(String... pathSpecs) {
- this(List.of(), List.of(pathSpecs));
- }
-
- PathGroup(Matcher first, String... pathSpecs) {
- this(List.of(first), List.of(pathSpecs));
- }
-
- PathGroup(Matcher first, Matcher second, String... pathSpecs) {
- this(List.of(first, second), List.of(pathSpecs));
- }
-
- PathGroup(Matcher first, Matcher second, Matcher third, String... pathSpecs) {
- this(List.of(first, second, third), List.of(pathSpecs));
- }
-
- /** Creates a new path group, if the given context matchers are each present exactly once in each of the given specs. */
- PathGroup(List<Matcher> matchers, List<String> pathSpecs) {
- this.matchers = matchers;
- this.pathSpecs = pathSpecs;
- }
-
- /** Returns path if it matches any spec in this group, with match groups set by the match. */
- private Optional<Path> get(URI uri) {
- Path matcher = new Path(uri);
- for (String spec : pathSpecs) // Iterate to be sure the Path's state is that of the match.
- if (matcher.matches(spec)) return Optional.of(matcher);
- return Optional.empty();
- }
-
- /** All known path groups */
- static Set<PathGroup> all() {
- return EnumSet.allOf(PathGroup.class);
- }
-
- static Set<PathGroup> allExcept(PathGroup... pathGroups) {
- return EnumSet.complementOf(EnumSet.copyOf(List.of(pathGroups)));
- }
-
- static Set<PathGroup> allExcept(Set<PathGroup> pathGroups) {
- return EnumSet.complementOf(EnumSet.copyOf(pathGroups));
- }
-
- static Set<PathGroup> operatorRestrictedPaths() {
- var paths = billingPathsNoToken();
- paths.add(accessRequestApproval);
- return paths;
- }
-
- static Set<PathGroup> billingPathsNoToken() {
- return EnumSet.of(PathGroup.billing, PathGroup.billingAux);
- }
-
- /** Returns whether this group matches path in given context */
- boolean matches(URI uri, Context context) {
- return get(uri).map(p -> {
- boolean match = true;
- String tenant = p.get(Matcher.tenant.name);
- if (tenant != null && context.tenant().isPresent()) {
- match = context.tenant().get().value().equals(tenant);
- }
- String application = p.get(Matcher.application.name);
- if (application != null && context.application().isPresent()) {
- match &= context.application().get().value().equals(application);
- }
- String instance = p.get(Matcher.instance.name);
- if (instance != null && context.instance().isPresent()) {
- match &= context.instance().get().value().equals(instance);
- }
- return match;
- }).orElse(false);
- }
-
-
- /** Fragments used to match parts of a path to create a context. */
- enum Matcher {
-
- tenant("{tenant}"),
- application("{application}"),
- instance("{instance}");
-
- final String pattern;
- final String name;
-
- Matcher(String pattern) {
- this.pattern = pattern;
- this.name = pattern.substring(1, pattern.length() - 1);
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java
deleted file mode 100644
index 0468be5f30c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Policy.java
+++ /dev/null
@@ -1,214 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-
-import java.net.URI;
-import java.util.Set;
-
-/**
- * Policies for REST APIs in the controller. A policy is only considered when defined in a {@link Role}.
- *
- * - A policy describes a set of {@link Privilege}s, which are valid for a set of {@link SystemName}s.
- * - A policy is evaluated by an {@link Enforcer}, which holds the {@link SystemName} the evaluation is done in.
- * - A policy is evaluated in a {@link Context}, which may limit it to a specific {@link TenantName} or
- * {@link ApplicationName}.
- *
- * @author mpolden
- */
-enum Policy {
-
- /** Full access to everything. */
- operator(Privilege.grant(Action.all())
- .on(PathGroup.allExcept(PathGroup.operatorRestrictedPaths()))
- .in(SystemName.all()),
- Privilege.grant(Action.read)
- .on(PathGroup.billingPathsNoToken())
- .in(SystemName.all())),
-
- /** Full access to everything. */
- supporter(Privilege.grant(Action.read)
- .on(PathGroup.allExcept(PathGroup.classifiedOperator, PathGroup.applicationRouting))
- .in(SystemName.all()),
- Privilege.grant(Action.all())
- .on(PathGroup.classifiedOperator, PathGroup.applicationRouting)
- .in(SystemName.all())),
-
- /** Full access to user management for a tenant in select systems. */
- tenantManager(Privilege.grant(Action.all())
- .on(PathGroup.tenantUsers)
- .in(SystemName.all())),
-
- /** Full access to user management for an application in select systems. */
- applicationManager(Privilege.grant(Action.all())
- .on(PathGroup.applicationUsers)
- .in(SystemName.all())),
-
- /** Access to create a user tenant in select systems. */
- user(Privilege.grant(Action.create, Action.update)
- .on(PathGroup.user)
- .in(SystemName.main, SystemName.cd)),
-
- /** Access to create a tenant. */
- tenantCreate(Privilege.grant(Action.create)
- .on(PathGroup.tenant)
- .in(SystemName.all())),
-
- /** Full access to tenant information and settings. */
- tenantDelete(Privilege.grant(Action.delete)
- .on(PathGroup.tenant)
- .in(SystemName.all())),
-
- /** Full access to tenant information and settings. */
- tenantUpdate(Privilege.grant(Action.update)
- .on(PathGroup.tenantInfo)
- .on(PathGroup.tenant)
- .in(SystemName.all())),
-
- /** Read access to tenant information and settings. */
- tenantRead(Privilege.grant(Action.read)
- .on(PathGroup.tenant, PathGroup.tenantInfo, PathGroup.tenantUsers, PathGroup.applicationUsers)
- .in(SystemName.all())),
-
- /** Access to set and unset archive access role under a tenant. */
- tenantArchiveAccessManagement(Privilege.grant(Action.update, Action.delete)
- .on(PathGroup.tenantArchiveAccess)
- .in(SystemName.all())),
-
- /** Access to create application under a certain tenant. */
- applicationCreate(Privilege.grant(Action.create)
- .on(PathGroup.application)
- .in(SystemName.all())),
-
- /** Read access to application information and settings. */
- applicationRead(Privilege.grant(Action.read)
- .on(PathGroup.application, PathGroup.applicationInfo, PathGroup.applicationRouting, PathGroup.reindexing, PathGroup.serviceDump, PathGroup.dropDocuments)
- .in(SystemName.all())),
-
- /** Update access to application information and settings. */
- applicationUpdate(Privilege.grant(Action.update)
- .on(PathGroup.application, PathGroup.applicationInfo, PathGroup.applicationRouting)
- .in(SystemName.all())),
-
- /** Access to delete a certain application. */
- applicationDelete(Privilege.grant(Action.delete)
- .on(PathGroup.application)
- .in(SystemName.all())),
-
- /** Full access to application information and settings. */
- applicationOperations(Privilege.grant(Action.write())
- .on(PathGroup.applicationInfo, PathGroup.applicationRouting, PathGroup.productionRestart, PathGroup.reindexing, PathGroup.serviceDump, PathGroup.dropDocuments)
- .in(SystemName.all())),
-
- /** Access to create and delete developer and deploy keys under a tenant. */
- keyManagement(Privilege.grant(Action.write())
- .on(PathGroup.tenantKeys, PathGroup.applicationKeys)
- .in(SystemName.all())),
-
- /** Access to revoke keys from the tenant */
- keyRevokal(Privilege.grant(Action.delete)
- .on(PathGroup.tenantKeys, PathGroup.applicationKeys)
- .in(SystemName.all())),
-
- /** Full access to application development deployments. */
- developmentDeployment(Privilege.grant(Action.all())
- .on(PathGroup.developmentDeployment, PathGroup.developmentRestart)
- .in(SystemName.all())),
-
- /** Read access to all application deployments. */
- deploymentRead(Privilege.grant(Action.read)
- .on(PathGroup.developmentDeployment, PathGroup.productionDeployment)
- .in(SystemName.all())),
-
- /** Full access to submissions for continuous deployment. */
- submission(Privilege.grant(Action.all())
- .on(PathGroup.submission)
- .in(SystemName.all())),
-
- /** Read access to all information in select systems. */
- classifiedRead(Privilege.grant(Action.read)
- .on(PathGroup.allExcept(PathGroup.classifiedOperator))
- .in(SystemName.main, SystemName.cd)),
-
- /** Read access to public info. */
- publicRead(Privilege.grant(Action.read)
- .on(PathGroup.publicInfo)
- .in(SystemName.all())),
-
- /** Access to /system-flags/v1/deploy. */
- systemFlagsDeploy(Privilege.grant(Action.update)
- .on(PathGroup.systemFlagsDeploy)
- .in(SystemName.all())),
-
- /** Access to /system-flags/v1/dryrun. */
- systemFlagsDryrun(Privilege.grant(Action.update)
- .on(PathGroup.systemFlagsDryrun)
- .in(SystemName.all())),
-
- /** Access to /payment/notification */
- paymentProcessor(Privilege.grant(Action.create)
- .on(PathGroup.paymentProcessor)
- .in(SystemName.PublicCd)),
-
- /** Ability to update tenant payment instrument */
- planUpdate(Privilege.grant(Action.update)
- .on(PathGroup.billing)
- .in(SystemName.PublicCd, SystemName.Public)),
-
- /** Read the generated bills */
- billingInformationRead(Privilege.grant(Action.read)
- .on(PathGroup.billing, PathGroup.billingAux)
- .in(SystemName.PublicCd, SystemName.Public)),
-
- accessRequests(Privilege.grant(Action.all())
- .on(PathGroup.accessRequests, PathGroup.accessRequestApproval)
- .in(SystemName.PublicCd, SystemName.Public)),
-
- /** Invoice management */
- hostedAccountant(Privilege.grant(Action.all())
- .on(PathGroup.accountant, PathGroup.userSearch)
- .in(SystemName.PublicCd, SystemName.Public)),
-
- /** Listing endpoint certificates and re-requesting certificates */
- endpointCertificateApi(Privilege.grant(Action.all())
- .on(PathGroup.endpointCertificates)
- .in(SystemName.all())),
-
- /** Secret store operations */
- secretStoreOperations(Privilege.grant(Action.all())
- .on(PathGroup.secretStore)
- .in(SystemName.PublicCd, SystemName.Public)),
-
- horizonProxyOperations(Privilege.grant(Action.all())
- .on(PathGroup.horizonProxy)
- .in(SystemName.PublicCd, SystemName.Public)),
-
- emailVerification(Privilege.grant(Action.create)
- .on(PathGroup.emailVerification)
- .in(SystemName.PublicCd, SystemName.Public)),
-
- dataplaneToken(Privilege.grant(Action.all())
- .on(PathGroup.dataplaneToken)
- .in(SystemName.PublicCd, SystemName.Public)),
-
- termsOfService(Privilege.grant(Action.create, Action.delete)
- .on(PathGroup.termsOfService)
- .in(SystemName.PublicCd, SystemName.Public));
-
- private final Set<Privilege> privileges;
-
- Policy(Privilege... privileges) {
- this.privileges = Set.of(privileges);
- }
-
- /** Returns whether action is allowed on path in given context */
- boolean evaluate(Action action, URI uri, Context context, SystemName system) {
- return privileges.stream().anyMatch(privilege -> privilege.actions().contains(action) &&
- privilege.systems().contains(system) &&
- privilege.pathGroups().stream()
- .anyMatch(pg -> pg.matches(uri, context)));
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Privilege.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Privilege.java
deleted file mode 100644
index 62a21132eb4..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Privilege.java
+++ /dev/null
@@ -1,99 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.config.provision.SystemName;
-
-import java.util.EnumSet;
-import java.util.LinkedHashSet;
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * This describes a privilege in the controller. A privilege expresses the actions (e.g. create or read) granted
- * for a particular group of REST API paths. A privilege is valid in one or more systems.
- *
- * @author mpolden
- */
-class Privilege {
-
- private final Set<SystemName> systems;
- private final Set<Action> actions;
- private final Set<PathGroup> pathGroups;
-
- private Privilege(Set<SystemName> systems, Set<Action> actions, Set<PathGroup> pathGroups) {
- this.systems = EnumSet.copyOf(Objects.requireNonNull(systems, "system must be non-null"));
- this.actions = EnumSet.copyOf(Objects.requireNonNull(actions, "actions must be non-null"));
- this.pathGroups = EnumSet.copyOf(Objects.requireNonNull(pathGroups, "pathGroups must be non-null"));
- if (systems.isEmpty()) {
- throw new IllegalArgumentException("systems must be non-empty");
- }
- }
-
- /** Systems where this applies */
- Set<SystemName> systems() {
- return systems;
- }
-
- /** Actions allowed by this */
- Set<Action> actions() {
- return actions;
- }
-
- /** Path groups where this applies */
- Set<PathGroup> pathGroups() {
- return pathGroups;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Privilege privilege = (Privilege) o;
- return systems.equals(privilege.systems) &&
- actions.equals(privilege.actions) &&
- pathGroups.equals(privilege.pathGroups);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(systems, actions, pathGroups);
- }
-
- static PrivilegeBuilder grant(Action... actions) {
- return grant(Set.of(actions));
- }
-
- static PrivilegeBuilder grant(Set<Action> actions) {
- return new PrivilegeBuilder(actions);
- }
-
- static class PrivilegeBuilder {
-
- private Set<Action> actions;
- private Set<PathGroup> pathGroups;
-
- private PrivilegeBuilder(Set<Action> actions) {
- this.actions = EnumSet.copyOf(actions);
- this.pathGroups = new LinkedHashSet<>();
- }
-
- PrivilegeBuilder on(PathGroup... pathGroups) {
- return on(Set.of(pathGroups));
- }
-
- PrivilegeBuilder on(Set<PathGroup> pathGroups) {
- this.pathGroups.addAll(pathGroups);
- return this;
- }
-
- Privilege in(SystemName... systems) {
- return in(Set.of(systems));
- }
-
- Privilege in(Set<SystemName> systems) {
- return new Privilege(systems, actions, pathGroups);
- }
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java
deleted file mode 100644
index 6149c2ad1bf..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/Role.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.TenantName;
-
-import java.util.Objects;
-
-/**
- * A role is a combination of a {@link RoleDefinition} and a {@link Context}, which allows evaluation
- * of access control for a given action on a resource.
- *
- * @author jonmv
- */
-public abstract class Role {
-
- private final RoleDefinition roleDefinition;
- final Context context;
-
- Role(RoleDefinition roleDefinition, Context context) {
- this.roleDefinition = Objects.requireNonNull(roleDefinition);
- this.context = Objects.requireNonNull(context);
- }
-
- /** Returns a {@link RoleDefinition#hostedOperator} for the current system. */
- public static UnboundRole hostedOperator() {
- return new UnboundRole(RoleDefinition.hostedOperator);
- }
-
- /** Returns a {@link RoleDefinition#hostedSupporter} for the current system. */
- public static UnboundRole hostedSupporter() {
- return new UnboundRole(RoleDefinition.hostedSupporter);
- }
-
- /** Returns a {@link RoleDefinition#everyone} for the current system. */
- public static UnboundRole everyone() {
- return new UnboundRole(RoleDefinition.everyone);
- }
-
- /** Returns a {@link RoleDefinition#athenzTenantAdmin} for the current system and given tenant. */
- public static TenantRole athenzTenantAdmin(TenantName tenant) {
- return new TenantRole(RoleDefinition.athenzTenantAdmin, tenant);
- }
-
- /** Returns a {@link RoleDefinition#reader} for the current system and given tenant. */
- public static TenantRole reader(TenantName tenant) {
- return new TenantRole(RoleDefinition.reader, tenant);
- }
-
- /** Returns a {@link RoleDefinition#developer} for the current system and given tenant. */
- public static TenantRole developer(TenantName tenant) {
- return new TenantRole(RoleDefinition.developer, tenant);
- }
-
- /** Returns a {@link RoleDefinition#hostedDeveloper} for the current system and given tenant. */
- public static TenantRole hostedDeveloper(TenantName tenant) {
- return new TenantRole(RoleDefinition.hostedDeveloper, tenant);
- }
-
- /** Returns a {@link RoleDefinition#administrator} for the current system and given tenant. */
- public static TenantRole administrator(TenantName tenant) {
- return new TenantRole(RoleDefinition.administrator, tenant);
- }
-
- /** Returns a {@link RoleDefinition#headless} for the current system, given tenant, and application */
- public static ApplicationRole headless(TenantName tenant, ApplicationName application) {
- return new ApplicationRole(RoleDefinition.headless, tenant, application);
- }
-
- /** Returns a {@link RoleDefinition#buildService} for the current system and given tenant and application. */
- public static ApplicationRole buildService(TenantName tenant, ApplicationName application) {
- return new ApplicationRole(RoleDefinition.buildService, tenant, application);
- }
-
- /** Returns the role for system flag deployer */
- public static UnboundRole systemFlagsDeployer() { return new UnboundRole(RoleDefinition.systemFlagsDeployer); }
-
- /** Returns the role for system flag dryrun */
- public static UnboundRole systemFlagsDryrunner() { return new UnboundRole(RoleDefinition.systemFlagsDryrunner); }
-
- /** Returns the role of the payment processor */
- public static UnboundRole paymentProcessor() { return new UnboundRole(RoleDefinition.paymentProcessor); }
-
- /** Returns the role of the invoice manager */
- public static UnboundRole hostedAccountant() { return new UnboundRole(RoleDefinition.hostedAccountant); }
-
- /** Returns the role definition of this bound role. */
- public RoleDefinition definition() { return roleDefinition; }
-
- /** Returns whether the other role is a parent of this, and has a context included in this role's context. */
- public boolean implies(Role other) {
- return (context.tenant().isEmpty() || context.tenant().equals(other.context.tenant()))
- && (context.application().isEmpty() || context.application().equals(other.context.application()))
- && roleDefinition.inherited().contains(other.roleDefinition);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Role role = (Role) o;
- return roleDefinition == role.roleDefinition &&
- Objects.equals(context, role.context);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(roleDefinition, context);
- }
-
-}
-
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
deleted file mode 100644
index 0b5359ac826..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/RoleDefinition.java
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import java.util.EnumSet;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * This declares all tenant roles known to the controller. A role contains one or more {@link Policy}s which decide
- * what actions a member of a role can perform, given a {@link Context} for the action.
- *
- * Optionally, some role definitions also inherit all policies from a "lower ranking" role.
- *
- * See {@link Role} for roles bound to a context, where policies can be evaluated.
- *
- * @author mpolden
- * @author jonmv
- */
-public enum RoleDefinition {
-
- /** Deus ex machina. */
- hostedOperator(Policy.operator),
-
- /** Machina autem exspiravit. */
- hostedSupporter(Policy.supporter),
-
- /** Base role which every user is part of. */
- everyone(Policy.classifiedRead,
- Policy.publicRead,
- Policy.user,
- Policy.tenantCreate,
- Policy.emailVerification),
-
- /** Build service which may submit new applications for continuous deployment. */
- buildService(everyone,
- Policy.tenantRead,
- Policy.applicationRead,
- Policy.deploymentRead,
- Policy.submission),
-
- /** Reader — the base role for all tenant users */
- reader(Policy.tenantRead,
- Policy.applicationRead,
- Policy.deploymentRead,
- Policy.publicRead,
- Policy.billingInformationRead,
- Policy.horizonProxyOperations),
-
- /** User — the dev.ops. role for normal Vespa tenant users */
- developer(Policy.applicationCreate,
- Policy.applicationUpdate,
- Policy.applicationDelete,
- Policy.applicationOperations,
- Policy.developmentDeployment,
- Policy.keyManagement,
- Policy.submission,
- Policy.billingInformationRead,
- Policy.secretStoreOperations,
- Policy.dataplaneToken),
-
- /** Developer for manual deployments for a tenant */
- hostedDeveloper(Policy.developmentDeployment),
-
- /** Admin — the administrative function for user management etc. */
- administrator(Policy.tenantUpdate,
- Policy.tenantManager,
- Policy.tenantDelete,
- Policy.tenantArchiveAccessManagement,
- Policy.applicationManager,
- Policy.keyRevokal,
- Policy.billingInformationRead,
- Policy.accessRequests,
- Policy.termsOfService
- ),
-
- /** Headless — the application specific role identified by deployment keys for production */
- headless(Policy.submission),
-
- /** Tenant administrator with full access to all child resources. */
- athenzTenantAdmin(everyone,
- Policy.tenantRead,
- Policy.tenantUpdate,
- Policy.tenantDelete,
- Policy.applicationCreate,
- Policy.applicationUpdate,
- Policy.applicationDelete,
- Policy.applicationOperations,
- Policy.keyManagement,
- Policy.developmentDeployment),
-
- systemFlagsDeployer(Policy.systemFlagsDeploy, Policy.systemFlagsDryrun),
-
- systemFlagsDryrunner(Policy.systemFlagsDryrun),
-
- paymentProcessor(Policy.paymentProcessor),
-
- hostedAccountant(Policy.hostedAccountant,
- Policy.planUpdate,
- Policy.tenantUpdate);
-
- private final Set<RoleDefinition> parents;
- private final Set<Policy> policies;
-
- RoleDefinition(Policy... policies) {
- this(Set.of(), policies);
- }
-
- RoleDefinition(RoleDefinition parent, Policy... policies) {
- this(Set.of(parent), policies);
- }
-
- RoleDefinition(Set<RoleDefinition> parents, Policy... policies) {
- this.parents = new HashSet<>(parents);
- this.policies = EnumSet.copyOf(Set.of(policies));
- for (RoleDefinition parent : parents) {
- this.parents.addAll(parent.parents);
- this.policies.addAll(parent.policies);
- }
- }
-
- Set<Policy> policies() {
- return policies;
- }
-
- Set<RoleDefinition> inherited() {
- return parents;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java
deleted file mode 100644
index 499f21a2a09..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SecurityContext.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import java.security.Principal;
-import java.time.Instant;
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * @author tokle
- */
-public class SecurityContext {
-
- public static final String ATTRIBUTE_NAME = SecurityContext.class.getName();
-
- private final Principal principal;
- private final Set<Role> roles;
- private final Instant issuedAt;
- private final Instant expiresAt;
-
- public SecurityContext(Principal principal, Set<Role> roles, Instant issuedAt) {
- this(principal, roles, issuedAt, Instant.MAX);
- }
-
- public SecurityContext(Principal principal, Set<Role> roles, Instant issuedAt, Instant expiresAt) {
- this.principal = Objects.requireNonNull(principal);
- this.roles = Set.copyOf(roles);
- this.issuedAt = Objects.requireNonNull(issuedAt);
- this.expiresAt = Objects.requireNonNull(expiresAt);
- }
-
- public SecurityContext(Principal principal, Set<Role> roles) {
- this(principal, roles, Instant.EPOCH, Instant.MAX);
- }
-
- public Principal principal() {
- return principal;
- }
-
- public Set<Role> roles() {
- return roles;
- }
-
- public Instant issuedAt() {
- return issuedAt;
- }
-
- /** @return credential expiration or {@link Instant#MAX} is not available */
- public Instant expiresAt() { return expiresAt; }
-
- public SecurityContext withRoles(Set<Role> roles) {
- return new SecurityContext(this.principal, roles, this.issuedAt, this.expiresAt);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- SecurityContext that = (SecurityContext) o;
- return Objects.equals(principal, that.principal) && Objects.equals(roles, that.roles)
- && Objects.equals(issuedAt, that.issuedAt) && Objects.equals(expiresAt, that.expiresAt);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(principal, roles, issuedAt, expiresAt);
- }
-
- @Override
- public String toString() {
- return "SecurityContext{" +
- "principal=" + principal +
- ", roles=" + roles +
- ", issuedAt=" + issuedAt +
- ", expiresAt=" + expiresAt +
- '}';
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SimplePrincipal.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SimplePrincipal.java
deleted file mode 100644
index 1ac43d4bb14..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/SimplePrincipal.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import java.security.Principal;
-
-/**
- * A principal wrapper of a single String entry.
- *
- * @author jonmv
- */
-public class SimplePrincipal implements Principal {
-
- private final String name;
-
- public SimplePrincipal(String name) {
- if (name.isBlank())
- throw new IllegalArgumentException("Name cannot be blank");
- this.name = name;
- }
-
- public static SimplePrincipal of(Principal principal) {
- return new SimplePrincipal(principal.getName());
- }
-
- @Override
- public String getName() {
- return name;
- }
-
- @Override
- public String toString() {
- return name;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- return name.equals(((SimplePrincipal) o).name);
- }
-
- @Override
- public int hashCode() {
- return name.hashCode();
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/TenantRole.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/TenantRole.java
deleted file mode 100644
index 878a3b9a2f2..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/TenantRole.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.config.provision.TenantName;
-
-/**
- * A {@link Role} with a {@link Context} of a {@link TenantName}.
- *
- * @author jonmv
- */
-public class TenantRole extends Role {
-
- TenantRole(RoleDefinition roleDefinition, TenantName tenant) {
- super(roleDefinition, Context.limitedTo(tenant));
- }
-
- /** Returns the {@link TenantName} this is bound to. */
- public TenantName tenant() { return context.tenant().get(); }
-
- @Override
- public String toString() {
- return "role '" + definition() + "' of '" + tenant() + "'";
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/UnboundRole.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/UnboundRole.java
deleted file mode 100644
index f01d6cbf602..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/UnboundRole.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-/**
- * A {@link Role} with an unlimited {@link Context}.
- *
- * @author jonmv
- */
-public class UnboundRole extends Role {
-
- UnboundRole(RoleDefinition roleDefinition) {
- super(roleDefinition, Context.unlimited());
- }
-
- @Override
- public String toString() {
- return "role '" + definition() + "'";
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/package-info.java
deleted file mode 100644
index 084e73da312..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/role/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ConfigServerFlagsTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ConfigServerFlagsTarget.java
deleted file mode 100644
index 996d6dbc519..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ConfigServerFlagsTarget.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1;
-
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.flags.json.FlagData;
-
-import java.net.URI;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-import static com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget.defaultFile;
-import static com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget.environmentFile;
-import static com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget.systemFile;
-import static com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget.zoneFile;
-
-/**
- * @author bjorncs
- */
-class ConfigServerFlagsTarget implements FlagsTarget {
- private final SystemName system;
- private final CloudName cloud;
- private final ZoneId zone;
- private final URI endpoint;
- private final AthenzIdentity identity;
-
- ConfigServerFlagsTarget(SystemName system, CloudName cloud, ZoneId zone, URI endpoint, AthenzIdentity identity) {
- this.system = Objects.requireNonNull(system);
- this.cloud = Objects.requireNonNull(cloud);
- this.zone = Objects.requireNonNull(zone);
- this.endpoint = Objects.requireNonNull(endpoint);
- this.identity = Objects.requireNonNull(identity);
- }
-
- @Override public List<String> flagDataFilesPrioritized() { return List.of(zoneFile(system, zone), environmentFile(system, zone.environment()), systemFile(system), defaultFile()); }
- @Override public URI endpoint() { return endpoint; }
- @Override public Optional<AthenzIdentity> athenzHttpsIdentity() { return Optional.of(identity); }
- @Override public String asString() { return String.format("%s.%s", system.value(), zone.value()); }
-
- @Override
- public FlagData partiallyResolveFlagData(FlagData data) {
- return FlagsTarget.partialResolve(data, system, cloud, zone);
- }
-
- @Override
- public String toString() {
- return "ConfigServerFlagsTarget{" +
- "system=" + system +
- ", cloud=" + cloud +
- ", zone=" + zone +
- ", endpoint=" + endpoint +
- ", identity=" + identity +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ConfigServerFlagsTarget that = (ConfigServerFlagsTarget) o;
- return system == that.system && cloud.equals(that.cloud) && zone.equals(that.zone) && endpoint.equals(that.endpoint) && identity.equals(that.identity);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(system, cloud, zone, endpoint, identity);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ControllerFlagsTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ControllerFlagsTarget.java
deleted file mode 100644
index e208e6c3ef8..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/ControllerFlagsTarget.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1;
-
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.flags.json.FlagData;
-
-import java.net.URI;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-import static com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget.controllerFile;
-import static com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget.defaultFile;
-import static com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget.systemFile;
-
-/**
- * @author bjorncs
- */
-class ControllerFlagsTarget implements FlagsTarget {
- private final SystemName system;
- private final CloudName cloud;
- private final ZoneId zone;
-
- ControllerFlagsTarget(SystemName system, CloudName cloud, ZoneId zone) {
- this.system = Objects.requireNonNull(system);
- this.cloud = Objects.requireNonNull(cloud);
- this.zone = Objects.requireNonNull(zone);
- }
-
- @Override public List<String> flagDataFilesPrioritized() { return List.of(controllerFile(system), systemFile(system), defaultFile()); }
- @Override public URI endpoint() { return URI.create("https://localhost:4443/"); } // Note: Cannot use VIPs for controllers due to network configuration on AWS
- @Override public Optional<AthenzIdentity> athenzHttpsIdentity() { return Optional.empty(); }
- @Override public String asString() { return String.format("%s.controller", system.value()); }
-
- @Override
- public FlagData partiallyResolveFlagData(FlagData data) {
- return FlagsTarget.partialResolve(data, system, cloud, zone);
- }
-
- @Override
- public String toString() {
- return "ControllerFlagsTarget{" +
- "system=" + system +
- ", cloud=" + cloud +
- ", zone=" + zone +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ControllerFlagsTarget that = (ControllerFlagsTarget) o;
- return system == that.system && cloud.equals(that.cloud) && zone.equals(that.zone);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(system, cloud, zone);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagValidationException.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagValidationException.java
deleted file mode 100644
index b844266be4b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagValidationException.java
+++ /dev/null
@@ -1,11 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1;
-
-/**
- * @author hakonhall
- */
-public class FlagValidationException extends RuntimeException {
- public FlagValidationException(String message) {
- super(message);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTarget.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTarget.java
deleted file mode 100644
index 051ddb3d8ea..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTarget.java
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1;
-
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.config.provision.zone.ZoneList;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagDefinition;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-
-import java.net.URI;
-import java.util.EnumSet;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-import static com.yahoo.vespa.flags.FetchVector.Dimension.CLOUD;
-import static com.yahoo.vespa.flags.FetchVector.Dimension.ENVIRONMENT;
-import static com.yahoo.vespa.flags.FetchVector.Dimension.SYSTEM;
-import static com.yahoo.vespa.flags.FetchVector.Dimension.ZONE_ID;
-
-/**
- * Represents either configservers in a zone or controllers in a system.
- *
- * Defines the location and precedence of the flags data files for the given target.
- *
- * Naming rules for flags data files:
- * <ul>
- * <li>zone specific: {@code <system>.<environment>.<region>.json}</li>
- * <li>controller specific: {@code <system>.controller.json}</li>
- * <li>environment specific: {@code <system>.<environment>.json}</li>
- * <li>system specific: {@code <system>.json}</li>
- * <li>global default: {@code default.json}</li>
- * </ul>
- *
- * @author bjorncs
- */
-public interface FlagsTarget {
-
- List<String> flagDataFilesPrioritized();
- URI endpoint();
- Optional<AthenzIdentity> athenzHttpsIdentity();
- String asString();
-
- FlagData partiallyResolveFlagData(FlagData data);
-
- static Set<FlagsTarget> getAllTargetsInSystem(ZoneRegistry registry, boolean reachableOnly) {
- Set<FlagsTarget> targets = new HashSet<>();
- ZoneList filteredZones = reachableOnly ? registry.zones().reachable() : registry.zones().all();
- for (ZoneApi zone : filteredZones.zones()) {
- targets.add(forConfigServer(registry, zone));
- }
- targets.add(forController(registry.systemZone()));
- return targets;
- }
-
- static FlagsTarget forController(ZoneApi controllerZone) {
- return new ControllerFlagsTarget(controllerZone.getSystemName(), controllerZone.getCloudName(), controllerZone.getVirtualId());
- }
-
- static FlagsTarget forConfigServer(ZoneRegistry registry, ZoneApi zone) {
- return new ConfigServerFlagsTarget(registry.system(),
- zone.getCloudName(),
- zone.getVirtualId(),
- registry.getConfigServerVipUri(zone.getVirtualId()),
- registry.getConfigServerHttpsIdentity(zone.getVirtualId()));
- }
-
- static String defaultFile() { return jsonFile("default"); }
- static String systemFile(SystemName system) { return jsonFile(system.value()); }
- static String environmentFile(SystemName system, Environment environment) { return jsonFile(system.value() + "." + environment); }
- static String zoneFile(SystemName system, ZoneId zone) { return jsonFile(system.value() + "." + zone.environment().value() + "." + zone.region().value()); }
- static String controllerFile(SystemName system) { return jsonFile(system.value() + ".controller"); }
-
- /** Return true if the filename applies to the system. Throws on invalid filename format. */
- static boolean filenameForSystem(String filename, SystemName system) throws FlagValidationException {
- if (filename.equals(defaultFile())) return true;
-
- String[] parts = filename.split("\\.", -1);
- if (parts.length < 2) throw new FlagValidationException("Invalid flag filename: " + filename);
-
- if (!parts[parts.length - 1].equals("json")) throw new FlagValidationException("Invalid flag filename: " + filename);
-
- SystemName systemFromFile;
- try {
- systemFromFile = SystemName.from(parts[0]);
- } catch (IllegalArgumentException e) {
- throw new FlagValidationException("First part of flag filename is neither 'default' nor a valid system: " + filename);
- }
- if (!SystemName.hostedVespa().contains(systemFromFile))
- throw new FlagValidationException("Unknown system in flag filename: " + filename);
- if (!systemFromFile.equals(system)) return false;
-
- if (parts.length == 2) return true; // systemFile
-
- if (parts.length == 3) {
- if (parts[1].equals("controller")) return true; // controllerFile
- try {
- Environment.from(parts[1]);
- } catch (IllegalArgumentException e) {
- throw new FlagValidationException("Invalid environment in flag filename: " + filename);
- }
- return true; // environmentFile
- }
-
- if (parts.length == 4) {
- try {
- Environment.from(parts[1]);
- } catch (IllegalArgumentException e) {
- throw new FlagValidationException("Invalid environment in flag filename: " + filename);
- }
- try {
- RegionName.from(parts[2]);
- } catch (IllegalArgumentException e) {
- throw new FlagValidationException("Invalid region in flag filename: " + filename);
- }
- return true; // zoneFile
- }
-
- throw new FlagValidationException("Invalid flag filename: " + filename);
- }
-
- /** Partially resolve inter-zone dimensions, except those dimensions defined by the flag for a controller zone. */
- static FlagData partialResolve(FlagData data, SystemName system, CloudName cloud, ZoneId virtualZoneId) {
- Set<FetchVector.Dimension> flagDimensions =
- virtualZoneId.equals(ZoneId.ofVirtualControllerZone()) ?
- Flags.getFlag(data.id())
- .map(FlagDefinition::getDimensions)
- .map(Set::copyOf)
- // E.g. testing: Assume unknown flag should resolve any and all dimensions below
- .orElse(EnumSet.noneOf(FetchVector.Dimension.class)) :
- EnumSet.noneOf(FetchVector.Dimension.class);
-
- var fetchVector = new FetchVector();
- if (!flagDimensions.contains(CLOUD)) fetchVector = fetchVector.with(CLOUD, cloud.value());
- if (!flagDimensions.contains(ENVIRONMENT)) fetchVector = fetchVector.with(ENVIRONMENT, virtualZoneId.environment().value());
- fetchVector = fetchVector.with(SYSTEM, system.value());
- if (!flagDimensions.contains(ZONE_ID)) fetchVector = fetchVector.with(ZONE_ID, virtualZoneId.value());
- return fetchVector.isEmpty() ? data : data.partialResolve(fetchVector);
- }
-
- private static String jsonFile(String nameWithoutExtension) { return nameWithoutExtension + ".json"; }
-}
-
-
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java
deleted file mode 100644
index 02bb669417c..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchive.java
+++ /dev/null
@@ -1,376 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.text.JSON;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagId;
-import com.yahoo.vespa.flags.json.Condition;
-import com.yahoo.vespa.flags.json.DimensionHelper;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.flags.json.RelationalCondition;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-
-import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.UncheckedIOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-import java.util.zip.ZipOutputStream;
-
-import static com.yahoo.config.provision.CloudName.AWS;
-import static com.yahoo.config.provision.CloudName.GCP;
-import static com.yahoo.config.provision.CloudName.YAHOO;
-import static com.yahoo.vespa.flags.FetchVector.Dimension.SYSTEM;
-import static com.yahoo.yolean.Exceptions.uncheck;
-
-/**
- * Represents a hierarchy of flag data files. See {@link FlagsTarget} for file naming convention.
- *
- * The flag files must reside in a 'flags/' root directory containing a directory for each flag name:
- * {@code ./flags/<flag-id>/*.json}
- *
- * Optionally, there can be an arbitrary number of directories "between" 'flags/' root directory and
- * the flag name directory:
- * {@code ./flags/onelevel/<flag-id>/*.json}
- * {@code ./flags/onelevel/anotherlevel/<flag-id>/*.json}
- *
- * @author bjorncs
- */
-public class SystemFlagsDataArchive {
-
- private static final ObjectMapper mapper = new ObjectMapper();
-
- private final Map<FlagId, Map<String, FlagData>> files;
-
- private SystemFlagsDataArchive(Map<FlagId, Map<String, FlagData>> files) {
- this.files = files;
- }
-
- public static SystemFlagsDataArchive fromZip(InputStream rawIn, ZoneRegistry zoneRegistry) {
- Builder builder = new Builder();
- try (ZipInputStream zipIn = new ZipInputStream(new BufferedInputStream(rawIn))) {
- ZipEntry entry;
- while ((entry = zipIn.getNextEntry()) != null) {
- String name = entry.getName();
- if (!entry.isDirectory() && name.startsWith("flags/")) {
- Path filePath = Paths.get(name);
- String fileContent = new String(zipIn.readAllBytes(), StandardCharsets.UTF_8);
- builder.maybeAddFile(filePath, fileContent, zoneRegistry, true);
- }
- }
- return builder.build();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- public static SystemFlagsDataArchive fromDirectory(Path directory, ZoneRegistry zoneRegistry, boolean simulateInController) {
- Path root = directory.toAbsolutePath();
- Path flagsDirectory = directory.resolve("flags");
- if (!Files.isDirectory(flagsDirectory)) {
- throw new FlagValidationException("Sub-directory 'flags' does not exist: " + flagsDirectory);
- }
- try (Stream<Path> directoryStream = Files.walk(flagsDirectory)) {
- Builder builder = new Builder();
- directoryStream.forEach(path -> {
- Path relativePath = root.relativize(path.toAbsolutePath());
- if (Files.isRegularFile(path)) {
- String fileContent = uncheck(() -> Files.readString(path, StandardCharsets.UTF_8));
- builder.maybeAddFile(relativePath, fileContent, zoneRegistry, simulateInController);
- }
- });
- return builder.build();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- public byte[] toZipBytes() {
- try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
- toZip(out);
- return out.toByteArray();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- public void toZip(OutputStream out) {
- ZipOutputStream zipOut = new ZipOutputStream(out);
- files.forEach((flagId, fileMap) -> {
- fileMap.forEach((filename, flagData) -> {
- uncheck(() -> {
- zipOut.putNextEntry(new ZipEntry(toFilePath(flagId, filename)));
- zipOut.write(flagData.serializeToUtf8Json());
- zipOut.closeEntry();
- });
- });
- });
- uncheck(zipOut::flush);
- }
-
- public List<FlagData> flagData(FlagsTarget target) {
- List<String> filenames = target.flagDataFilesPrioritized();
- List<FlagData> targetData = new ArrayList<>();
- files.forEach((flagId, fileMap) -> {
- for (String filename : filenames) {
- FlagData data = fileMap.get(filename);
- if (data != null) {
- if (!data.isEmpty()) {
- targetData.add(data);
- }
- return;
- }
- }
- });
- return targetData;
- }
-
- public void validateAllFilesAreForTargets(Set<FlagsTarget> targets) throws FlagValidationException {
- Set<String> validFiles = targets.stream()
- .flatMap(target -> target.flagDataFilesPrioritized().stream())
- .collect(Collectors.toSet());
- files.forEach((flagId, fileMap) -> fileMap.keySet().forEach(filename -> {
- if (!validFiles.contains(filename)) {
- throw new FlagValidationException("Unknown flag file: " + toFilePath(flagId, filename));
- }
- }));
- }
-
- boolean hasFlagData(FlagId flagId, String filename) {
- return files.getOrDefault(flagId, Map.of()).containsKey(filename);
- }
-
- private static void validateSystems(FlagData flagData) throws FlagValidationException {
- flagData.rules().forEach(rule -> rule.conditions().forEach(condition -> {
- if (condition.dimension() == SYSTEM) {
- validateConditionValues(condition, system -> {
- if (!SystemName.hostedVespa().contains(SystemName.from(system)))
- throw new FlagValidationException("Unknown system: " + system);
- });
- }
- }));
- }
-
- private static void validateForSystem(FlagData flagData, ZoneRegistry zoneRegistry, boolean inController) throws FlagValidationException {
- Set<ZoneId> zones = inController ?
- zoneRegistry.zonesIncludingSystem().all().zones().stream().map(ZoneApi::getVirtualId).collect(Collectors.toSet()) :
- null;
-
- flagData.rules().forEach(rule -> rule.conditions().forEach(condition -> {
- int force_switch_expression_dummy = switch (condition.type()) {
- case RELATIONAL -> switch (condition.dimension()) {
- case APPLICATION, CLOUD, CLOUD_ACCOUNT, CLUSTER_ID, CLUSTER_TYPE, CONSOLE_USER_EMAIL,
- ENVIRONMENT, HOSTNAME, INSTANCE_ID, NODE_TYPE, SYSTEM, TENANT_ID, ZONE_ID ->
- throw new FlagValidationException(condition.type().toWire() + " " +
- DimensionHelper.toWire(condition.dimension()) +
- " condition is not supported");
- case VESPA_VERSION -> {
- RelationalCondition rCond = RelationalCondition.create(condition.toCreateParams());
- Version version = Version.fromString(rCond.relationalPredicate().rightOperand());
- if (version.getMajor() < 8)
- throw new FlagValidationException("Major Vespa version must be at least 8: " + version);
- yield 0;
- }
- };
-
- case WHITELIST, BLACKLIST -> switch (condition.dimension()) {
- case APPLICATION -> validateConditionValues(condition, SystemFlagsDataArchive::validateTenantApplication);
- case CONSOLE_USER_EMAIL -> validateConditionValues(condition, email -> {
- if (!email.contains("@"))
- throw new FlagValidationException("Invalid email address: " + email);
- });
- case CLOUD -> validateConditionValues(condition, cloud -> {
- if (!Set.of(YAHOO, AWS, GCP).contains(CloudName.from(cloud)))
- throw new FlagValidationException("Unknown cloud: " + cloud);
- });
- case CLOUD_ACCOUNT -> validateConditionValues(condition, CloudAccount::from);
- case CLUSTER_ID -> validateConditionValues(condition, ClusterSpec.Id::from);
- case CLUSTER_TYPE -> validateConditionValues(condition, ClusterSpec.Type::from);
- case ENVIRONMENT -> validateConditionValues(condition, Environment::from);
- case HOSTNAME -> validateConditionValues(condition, HostName::of);
- case INSTANCE_ID -> validateConditionValues(condition, ApplicationId::fromSerializedForm);
- case NODE_TYPE -> validateConditionValues(condition, NodeType::valueOf);
- case SYSTEM -> throw new IllegalStateException("Flag data contains system dimension");
- case TENANT_ID -> validateConditionValues(condition, TenantName::from);
- case VESPA_VERSION -> throw new FlagValidationException(condition.type().toWire() + " " +
- DimensionHelper.toWire(condition.dimension()) +
- " condition is not supported");
- case ZONE_ID -> validateConditionValues(condition, zoneIdString -> {
- ZoneId zoneId = ZoneId.from(zoneIdString);
- if (inController && !zones.contains(zoneId))
- throw new FlagValidationException("Unknown zone: " + zoneIdString);
- });
- };
- };
- }));
- }
-
- private static int validateConditionValues(Condition condition, Consumer<String> valueValidator) {
- condition.toCreateParams().values().forEach(value -> {
- try {
- valueValidator.accept(value);
- } catch (IllegalArgumentException e) {
- String dimension = DimensionHelper.toWire(condition.dimension());
- String type = condition.type().toWire();
- throw new FlagValidationException("Invalid %s '%s' in %s condition: %s".formatted(dimension, value, type, e.getMessage()));
- }
- });
-
- return 0; // dummy to force switch expression
- }
-
- private static void validateTenantApplication(String application) {
- String[] parts = application.split(":");
- if (parts.length != 2)
- throw new IllegalArgumentException("Applications must be on the form tenant:application, but was %s".formatted(application));
- TenantName.from(parts[0]);
- ApplicationName.from(parts[1]);
- }
-
- private static FlagData parseFlagData(FlagId flagId, String fileContent, ZoneRegistry zoneRegistry, boolean inController) {
- if (fileContent.isBlank()) return new FlagData(flagId);
-
- final JsonNode root;
- try {
- root = mapper.readTree(fileContent);
- } catch (JsonProcessingException e) {
- throw new FlagValidationException("Invalid JSON: " + e.getMessage());
- }
-
- removeCommentsRecursively(root);
- removeNullRuleValues(root);
- String normalizedRawData = root.toString();
- FlagData flagData = FlagData.deserialize(normalizedRawData);
-
- if (!flagId.equals(flagData.id()))
- throw new FlagValidationException("Flag ID specified in file (%s) doesn't match the directory name (%s)"
- .formatted(flagData.id(), flagId.toString()));
-
- String serializedData = flagData.serializeToJson();
- if (!JSON.equals(serializedData, normalizedRawData))
- throw new FlagValidationException("""
- Unknown non-comment fields or rules with null values: after removing any comment fields the JSON is:
- %s
- but deserializing this ended up with:
- %s
- These fields may be spelled wrong, or remove them?
- See https://git.ouroath.com/vespa/hosted-feature-flags for more info on the JSON syntax
- """.formatted(normalizedRawData, serializedData));
-
- validateSystems(flagData);
- flagData = flagData.partialResolve(new FetchVector().with(SYSTEM, zoneRegistry.system().value()));
-
- validateForSystem(flagData, zoneRegistry, inController);
-
- return flagData;
- }
-
- private static void removeCommentsRecursively(JsonNode node) {
- if (node instanceof ObjectNode) {
- ObjectNode objectNode = (ObjectNode) node;
- objectNode.remove("comment");
- }
-
- node.forEach(SystemFlagsDataArchive::removeCommentsRecursively);
- }
-
- private static void removeNullRuleValues(JsonNode root) {
- if (root instanceof ObjectNode objectNode) {
- JsonNode rules = objectNode.get("rules");
- if (rules != null) {
- rules.forEach(ruleNode -> {
- if (ruleNode instanceof ObjectNode rule) {
- JsonNode value = rule.get("value");
- if (value != null && value.isNull()) {
- rule.remove("value");
- }
- }
- });
- }
- }
- }
-
- private static String toFilePath(FlagId flagId, String filename) {
- return "flags/" + flagId.toString() + "/" + filename;
- }
-
- public static class Builder {
- private final Map<FlagId, Map<String, FlagData>> files = new TreeMap<>();
-
- public Builder() {}
-
- boolean maybeAddFile(Path filePath, String fileContent, ZoneRegistry zoneRegistry, boolean inController) {
- String filename = filePath.getFileName().toString();
-
- if (filename.startsWith("."))
- return false; // Ignore files starting with '.'
-
- if (!inController && !FlagsTarget.filenameForSystem(filename, zoneRegistry.system()))
- return false; // Ignore files for other systems
-
- FlagId directoryDeducedFlagId = new FlagId(filePath.getName(filePath.getNameCount()-2).toString());
-
- if (hasFile(filename, directoryDeducedFlagId))
- throw new FlagValidationException("Flag data file in '%s' contains redundant flag data for id '%s' already set in another directory!"
- .formatted(filePath, directoryDeducedFlagId));
-
- final FlagData flagData;
- try {
- flagData = parseFlagData(directoryDeducedFlagId, fileContent, zoneRegistry, inController);
- } catch (FlagValidationException e) {
- throw new FlagValidationException("In file " + filePath + ": " + e.getMessage());
- }
-
- addFile(filename, flagData);
- return true;
- }
-
- public Builder addFile(String filename, FlagData data) {
- files.computeIfAbsent(data.id(), k -> new TreeMap<>()).put(filename, data);
- return this;
- }
-
- public boolean hasFile(String filename, FlagId id) {
- return files.containsKey(id) && files.get(id).containsKey(filename);
- }
-
- public SystemFlagsDataArchive build() {
- Map<FlagId, Map<String, FlagData>> copy = new TreeMap<>();
- files.forEach((flagId, map) -> copy.put(flagId, new TreeMap<>(map)));
- return new SystemFlagsDataArchive(copy);
- }
-
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/package-info.java
deleted file mode 100644
index c6aba260d13..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/package-info.java
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author bjorncs
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/SystemFlagsV1Api.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/SystemFlagsV1Api.java
deleted file mode 100644
index 6b63d19e952..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/SystemFlagsV1Api.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.MediaType;
-import java.io.InputStream;
-
-/**
- * @author bjorncs
- */
-@Path("/system-flags/v1")
-public interface SystemFlagsV1Api {
-
- @PUT
- @Produces(MediaType.APPLICATION_JSON)
- @Consumes("application/zip")
- @Path("/deploy")
- WireSystemFlagsDeployResult deploy(InputStream inputStream);
-
- @PUT
- @Produces(MediaType.APPLICATION_JSON)
- @Consumes("application/zip")
- @Path("/dryrun")
- WireSystemFlagsDeployResult dryrun(InputStream inputStream);
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/WireErrorResponse.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/WireErrorResponse.java
deleted file mode 100644
index c6eb55327af..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/WireErrorResponse.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * @author bjorncs
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class WireErrorResponse {
- @JsonProperty("message")
- public String message;
- @JsonProperty("error-code")
- public String errorCode;
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/WireSystemFlagsDeployResult.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/WireSystemFlagsDeployResult.java
deleted file mode 100644
index 9aae9c605b9..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/WireSystemFlagsDeployResult.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.vespa.flags.json.wire.WireFlagData;
-
-import java.util.List;
-
-/**
- * Note: This class is only annotated for serialization, deserialization is not supported.
- *
- * @author bjorncs
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class WireSystemFlagsDeployResult {
- @JsonProperty("changes") public List<WireFlagDataChange> changes;
- @JsonProperty("errors") public List<WireOperationFailure> errors;
- @JsonProperty("warnings") public List<WireWarning> warnings;
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- @JsonInclude(JsonInclude.Include.NON_NULL)
- public static class WireFlagDataChange {
- @JsonProperty("flag-id") public String flagId;
- @JsonProperty("owners") @JsonInclude(JsonInclude.Include.NON_EMPTY) public List<String> owners;
- @JsonProperty("targets") public List<String> targets;
- @JsonProperty("operation") public String operation;
- @JsonProperty("data") public WireFlagData data;
- @JsonProperty("previous-data") public WireFlagData previousData;
- }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- @JsonInclude(JsonInclude.Include.NON_NULL)
- public static class WireOperationFailure {
- @JsonProperty("flag-id") public String flagId;
- @JsonProperty("owners") @JsonInclude(JsonInclude.Include.NON_EMPTY) public List<String> owners;
- @JsonProperty("message") public String message;
- @JsonProperty("targets") public List<String> targets;
- @JsonProperty("operation") public String operation;
- @JsonProperty("data") public WireFlagData data;
- }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- @JsonInclude(JsonInclude.Include.NON_NULL)
- public static class WireWarning {
- @JsonProperty("flag-id") public String flagId;
- @JsonProperty("owners") @JsonInclude(JsonInclude.Include.NON_EMPTY) public List<String> owners;
- @JsonProperty("message") public String message;
- @JsonProperty("targets") public List<String> targets;
- }
-
- public boolean hasErrors() { return errors != null && !errors.isEmpty(); }
-}
-
-
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/package-info.java
deleted file mode 100644
index c560c2902d8..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/wire/package-info.java
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author bjorncs
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java
deleted file mode 100644
index 41d6e8264f6..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccess.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import com.amazonaws.arn.Arn;
-import com.yahoo.text.Text;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-public class ArchiveAccess {
-
- private static final Pattern VALID_GCP_ARCHIVE_ACCESS_MEMBER_PATTERN = Pattern.compile("(?<prefix>[a-zA-Z]+):.+");
-
- private static final Set<String> gcpMemberPrefixes = Set.of("user", "serviceAccount", "group", "domain");
-
- // AWS IAM Role
- private final Optional<String> awsRole;
- // GCP Member
- private final Optional<String> gcpMember;
-
- public ArchiveAccess() {
- this(Optional.empty(), Optional.empty());
- }
-
- private ArchiveAccess(Optional<String> awsRole, Optional<String> gcpMember) {
- this.awsRole = awsRole;
- this.gcpMember = gcpMember;
-
- awsRole.ifPresent(role -> validateAWSIAMRole(role));
- gcpMember.ifPresent(member -> validateGCPMember(member));
- }
-
- public ArchiveAccess withAWSRole(String role) {
- return new ArchiveAccess(Optional.of(role), gcpMember());
- }
-
- public ArchiveAccess withGCPMember(String member) {
- return new ArchiveAccess(awsRole(), Optional.of(member));
- }
-
- public ArchiveAccess withAWSRole(Optional<String> role) {
- return new ArchiveAccess(role, gcpMember());
- }
-
- public ArchiveAccess withGCPMember(Optional<String> member) {
- return new ArchiveAccess(awsRole(), member);
- }
-
- public ArchiveAccess removeAWSRole() {
- return new ArchiveAccess(Optional.empty(), gcpMember());
- }
-
- public ArchiveAccess removeGCPMember() {
- return new ArchiveAccess(awsRole(), Optional.empty());
- }
-
- private static final Pattern ACCOUNT_ID_PATTERN = Pattern.compile("\\d{12}");
- private static void validateAWSIAMRole(String role) {
- if (role.length() > 100) {
- throw new IllegalArgumentException("Invalid archive access role too long, must be 100 or less characters");
- }
- try {
- var arn = Arn.fromString(role);
- if (!arn.getPartition().equals("aws")) throw new IllegalArgumentException("Partition must be 'aws'");
- if (!arn.getService().equals("iam")) throw new IllegalArgumentException("Service must be 'iam'");
- var resourceType = arn.getResource().getResourceType();
- if (resourceType == null) throw new IllegalArgumentException("Missing resource type - must be 'role' or 'user'");
- if (!List.of("user", "role").contains(resourceType))
- throw new IllegalArgumentException("Invalid resource type - must be either a 'role' or 'user'");
- var accountId = arn.getAccountId();
- if (!ACCOUNT_ID_PATTERN.matcher(accountId).matches())
- throw new IllegalArgumentException("Account id must be a 12-digit number");
- } catch (IllegalArgumentException e) {
- throw new IllegalArgumentException(Text.format("Invalid archive access IAM role '%s': %s", role, e.getMessage()));
- }
- }
-
- private void validateGCPMember(String member) {
- var matcher = VALID_GCP_ARCHIVE_ACCESS_MEMBER_PATTERN.matcher(member);
- if (!matcher.matches()) {
- throw new IllegalArgumentException(Text.format("Invalid GCP archive access member '%s': Must match expected pattern: '%s'",
- gcpMember.get(), VALID_GCP_ARCHIVE_ACCESS_MEMBER_PATTERN.pattern()));
- }
- var prefix = matcher.group("prefix");
- if (!gcpMemberPrefixes.contains(prefix)) {
- throw new IllegalArgumentException(Text.format("Invalid GCP member prefix '%s', must be one of '%s'",
- prefix, gcpMemberPrefixes.stream().collect(Collectors.joining(", "))));
- }
- if (!"domain".equals(prefix) && !member.contains("@")) {
- throw new IllegalArgumentException(Text.format("Invalid GCP member '%s', prefix '%s' must be followed by an email id", member, prefix));
- }
- }
-
- public Optional<String> awsRole() {
- return awsRole;
- }
-
- public Optional<String> gcpMember() {
- return gcpMember;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ArchiveAccess that = (ArchiveAccess) o;
- return awsRole.equals(that.awsRole) && gcpMember.equals(that.gcpMember);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(awsRole, gcpMember);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java
deleted file mode 100644
index db72d17cd82..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/AthenzTenant.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Represents an Athenz tenant in hosted Vespa.
- *
- * @author mpolden
- */
-public class AthenzTenant extends Tenant {
-
- private final AthenzDomain domain;
- private final Property property;
- private final Optional<PropertyId> propertyId;
-
- /**
- * This should only be used by serialization.
- * Use {@link #create(TenantName, AthenzDomain, Property, Optional, Instant)}.
- * */
- public AthenzTenant(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId,
- Optional<Contact> contact, Instant createdAt, LastLoginInfo lastLoginInfo, Instant tenantRolesLastMaintained,
- List<CloudAccountInfo> cloudAccounts) {
- super(name, createdAt, lastLoginInfo, contact, tenantRolesLastMaintained, cloudAccounts);
- this.domain = Objects.requireNonNull(domain, "domain must be non-null");
- this.property = Objects.requireNonNull(property, "property must be non-null");
- this.propertyId = Objects.requireNonNull(propertyId, "propertyId must be non-null");
- }
-
- /** Property name of this tenant */
- public Property property() {
- return property;
- }
-
- /** Property ID of the tenant, if any */
- public Optional<PropertyId> propertyId() {
- return propertyId;
- }
-
- /** Athenz domain of this tenant */
- public AthenzDomain domain() {
- return domain;
- }
-
- /** Returns true if tenant is in given domain */
- public boolean in(AthenzDomain domain) {
- return this.domain.equals(domain);
- }
-
- @Override
- public String toString() {
- return "athenz tenant '" + name() + "'";
- }
-
- /** Create a new Athenz tenant */
- public static AthenzTenant create(TenantName name, AthenzDomain domain, Property property,
- Optional<PropertyId> propertyId, Instant createdAt) {
- return new AthenzTenant(requireName(name), domain, property, propertyId, Optional.empty(), createdAt, LastLoginInfo.EMPTY, Instant.EPOCH, List.of());
- }
-
- @Override
- public Type type() {
- return Type.athenz;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/BillingReference.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/BillingReference.java
deleted file mode 100644
index 52e842ed7ad..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/BillingReference.java
+++ /dev/null
@@ -1,7 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import java.time.Instant;
-
-public record BillingReference(String reference, Instant updated) {
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudAccountInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudAccountInfo.java
deleted file mode 100644
index 5ee54508213..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudAccountInfo.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudAccount;
-
-import java.util.Objects;
-
-/**
- * @author freva
- */
-public record CloudAccountInfo(CloudAccount cloudAccount, Version templateVersion) {
-
- public CloudAccountInfo {
- Objects.requireNonNull(cloudAccount, "cloudAccount must be non-null");
- Objects.requireNonNull(templateVersion, "templateVersion must be non-null");
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
deleted file mode 100644
index 9ceeba32061..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/CloudTenant.java
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import com.google.common.collect.BiMap;
-import com.google.common.collect.ImmutableBiMap;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-
-import java.security.Principal;
-import java.security.PublicKey;
-import java.time.Instant;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * A paying tenant in a Vespa cloud service.
- *
- * @author jonmv
- */
-public class CloudTenant extends Tenant {
-
- private final Optional<SimplePrincipal> creator;
- private final BiMap<PublicKey, SimplePrincipal> developerKeys;
- private final TenantInfo info;
- private final List<TenantSecretStore> tenantSecretStores;
- private final ArchiveAccess archiveAccess;
- private final Optional<Instant> invalidateUserSessionsBefore;
- private final Optional<BillingReference> billingReference;
- private final PlanId planId;
-
- /** Public for the serialization layer — do not use! */
- public CloudTenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<SimplePrincipal> creator,
- BiMap<PublicKey, SimplePrincipal> developerKeys, TenantInfo info,
- List<TenantSecretStore> tenantSecretStores, ArchiveAccess archiveAccess,
- Optional<Instant> invalidateUserSessionsBefore, Instant tenantRoleLastMaintained,
- List<CloudAccountInfo> cloudAccounts, Optional<BillingReference> billingReference,
- PlanId planId) {
- super(name, createdAt, lastLoginInfo, Optional.empty(), tenantRoleLastMaintained, cloudAccounts);
- this.creator = creator;
- this.developerKeys = developerKeys;
- this.info = Objects.requireNonNull(info);
- this.tenantSecretStores = tenantSecretStores;
- this.archiveAccess = Objects.requireNonNull(archiveAccess);
- this.invalidateUserSessionsBefore = invalidateUserSessionsBefore;
- this.billingReference = Objects.requireNonNull(billingReference);
- this.planId = Objects.requireNonNull(planId);
- }
-
- /** Creates a tenant with the given name, provided it passes validation. */
- public static CloudTenant create(TenantName tenantName, Instant createdAt, Principal creator) {
- // Initialize with creator as verified contact
- var info = TenantInfo.empty().withContacts(new TenantContacts(List.of(
- new TenantContacts.EmailContact(
- List.of(TenantContacts.Audience.TENANT, TenantContacts.Audience.NOTIFICATIONS),
- new Email(creator.getName(), true)))));
- return new CloudTenant(requireName(tenantName),
- createdAt,
- LastLoginInfo.EMPTY,
- Optional.ofNullable(creator).map(SimplePrincipal::of),
- ImmutableBiMap.of(), info, List.of(), new ArchiveAccess(), Optional.empty(),
- Instant.EPOCH, List.of(), Optional.empty(), PlanId.from("none"));
- }
-
- /** The user that created the tenant */
- public Optional<SimplePrincipal> creator() {
- return creator;
- }
-
- /** Legal name, addresses etc */
- public TenantInfo info() {
- return info;
- }
-
- /** Returns the set of developer keys and their corresponding developers for this tenant. */
- public BiMap<PublicKey, SimplePrincipal> developerKeys() { return developerKeys; }
-
- /** List of configured secret stores */
- public List<TenantSecretStore> tenantSecretStores() {
- return tenantSecretStores;
- }
-
- /**
- * Role or member that is allowed to access archive bucket (log, dump)
- *
- * For AWS is this the IAM role
- * For GCP it is a GCP member
- */
- public ArchiveAccess archiveAccess() {
- return archiveAccess;
- }
-
- /** Returns instant before which all user sessions that have access to this tenant must be refreshed */
- public Optional<Instant> invalidateUserSessionsBefore() {
- return invalidateUserSessionsBefore;
- }
-
- public Optional<BillingReference> billingReference() {
- return billingReference;
- }
-
- public PlanId planId() { return planId; }
-
- @Override
- public Type type() {
- return Type.cloud;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java
deleted file mode 100644
index 21b06839a1f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/DeletedTenant.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import com.yahoo.config.provision.TenantName;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Represents a tenant that has been deleted. Exists to prevent creation of a new tenant with the same name.
- *
- * @author freva
- */
-public class DeletedTenant extends Tenant {
-
- private final Instant deletedAt;
-
- public DeletedTenant(TenantName name, Instant createdAt, Instant deletedAt) {
- super(name, createdAt, LastLoginInfo.EMPTY, Optional.empty(), Instant.EPOCH, List.of());
- this.deletedAt = Objects.requireNonNull(deletedAt, "deletedAt must be non-null");
- }
-
- /** Instant when the tenant was deleted */
- public Instant deletedAt() {
- return deletedAt;
- }
-
- @Override
- public String toString() {
- return "deleted tenant '" + name() + "'";
- }
-
- @Override
- public Type type() {
- return Type.deleted;
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Email.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Email.java
deleted file mode 100644
index 702a183e7af..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Email.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import java.util.Objects;
-
-/**
- * @author olaa
- */
-public class Email {
-
- private final String emailAddress;
- private final boolean isVerified;
-
- public Email(String emailAddress, boolean isVerified) {
- this.emailAddress = emailAddress;
- this.isVerified = isVerified;
- }
-
- public String getEmailAddress() {
- return emailAddress;
- }
-
- public boolean isVerified() {
- return isVerified;
- }
-
- public static Email empty() {
- return new Email("", false);
- }
-
- public Email withEmailAddress(String emailAddress) {
- return new Email(emailAddress, isVerified);
- }
-
- public Email withVerification(boolean isVerified) {
- return new Email(emailAddress, isVerified);
- }
-
- public boolean isBlank() {
- return emailAddress.isBlank();
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Email email = (Email) o;
- return isVerified == email.isVerified && Objects.equals(emailAddress, email.emailAddress);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(emailAddress, isVerified);
- }
-
- @Override
- public String toString() {
- return emailAddress;
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java
deleted file mode 100644
index 5478421351b..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/LastLoginInfo.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * @author freva
- */
-public class LastLoginInfo {
-
- public static final LastLoginInfo EMPTY = new LastLoginInfo(Map.of());
-
- private final Map<UserLevel, Instant> lastLoginByUserLevel;
-
- public LastLoginInfo(Map<UserLevel, Instant> lastLoginByUserLevel) {
- this.lastLoginByUserLevel = Map.copyOf(lastLoginByUserLevel);
- }
-
- public Optional<Instant> get(UserLevel userLevel) {
- return Optional.ofNullable(lastLoginByUserLevel.get(userLevel));
- }
-
- /**
- * Returns new instance with updated last login time if the given {@code loginAt} timestamp is after the current
- * for the given {@code userLevel}, otherwise returns this
- */
- public LastLoginInfo withLastLoginIfLater(UserLevel userLevel, Instant loginAt) {
- Instant lastLogin = lastLoginByUserLevel.getOrDefault(userLevel, Instant.EPOCH);
- if (loginAt.isAfter(lastLogin)) {
- Map<UserLevel, Instant> lastLoginByUserLevel = new HashMap<>(this.lastLoginByUserLevel);
- lastLoginByUserLevel.put(userLevel, loginAt);
- return new LastLoginInfo(lastLoginByUserLevel);
- }
- return this;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- LastLoginInfo lastLoginInfo = (LastLoginInfo) o;
- return lastLoginByUserLevel.equals(lastLoginInfo.lastLoginByUserLevel);
- }
-
- @Override
- public int hashCode() {
- return lastLoginByUserLevel.hashCode();
- }
-
- public enum UserLevel { user, developer, administrator };
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PendingMailVerification.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PendingMailVerification.java
deleted file mode 100644
index 9c4bbc88f1f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PendingMailVerification.java
+++ /dev/null
@@ -1,82 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import com.yahoo.config.provision.TenantName;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * @author olaa
- */
-public class PendingMailVerification {
-
- private final TenantName tenantName;
- private final String mailAddress;
- private final String verificationCode;
- private final Instant verificationDeadline;
- private final MailType mailType;
-
- public PendingMailVerification(TenantName tenantName, String mailAddress, String verificationCode, Instant verificationDeadline, MailType mailType) {
- this.tenantName = tenantName;
- this.mailAddress = mailAddress;
- this.verificationCode = verificationCode;
- this.verificationDeadline = verificationDeadline;
- this.mailType = mailType;
- }
-
- public TenantName getTenantName() {
- return tenantName;
- }
-
- public String getMailAddress() {
- return mailAddress;
- }
-
- public String getVerificationCode() {
- return verificationCode;
- }
-
- public Instant getVerificationDeadline() {
- return verificationDeadline;
- }
-
- public MailType getMailType() {
- return mailType;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- PendingMailVerification that = (PendingMailVerification) o;
- return Objects.equals(tenantName, that.tenantName) &&
- Objects.equals(mailAddress, that.mailAddress) &&
- Objects.equals(verificationCode, that.verificationCode) &&
- Objects.equals(verificationDeadline, that.verificationDeadline) &&
- mailType == that.mailType;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(tenantName, mailAddress, verificationCode, verificationDeadline, mailType);
- }
-
- @Override
- public String toString() {
- return "PendingMailVerification{" +
- "tenantName=" + tenantName +
- ", mailAddress='" + mailAddress + '\'' +
- ", verificationCode='" + verificationCode + '\'' +
- ", verificationDeadline=" + verificationDeadline +
- ", mailType=" + mailType +
- '}';
- }
-
- public enum MailType {
- TENANT_CONTACT,
- NOTIFICATIONS,
- BILLING
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PurchaseOrder.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PurchaseOrder.java
deleted file mode 100644
index 04bf6301e81..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/PurchaseOrder.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import ai.vespa.validation.StringWrapper;
-
-import static ai.vespa.validation.Validation.requireLength;
-
-/**
- * @author olaa
- */
-public class PurchaseOrder extends StringWrapper<PurchaseOrder> {
-
- public PurchaseOrder(String value) {
- super(value);
- requireLength(value, "purchase order length", 0, 64);
- }
-
- public static PurchaseOrder empty() {
- return new PurchaseOrder("");
- }
- public boolean isEmpty() { return value().isEmpty(); }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TaxId.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TaxId.java
deleted file mode 100644
index 172ad257f6a..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TaxId.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import ai.vespa.validation.StringWrapper;
-
-import static ai.vespa.validation.Validation.requireLength;
-
-/**
- * @author olaa
- */
-public record TaxId(Country country, Type type, Code code) {
-
- public TaxId(String country, String type, String code) { this(new Country(country), new Type(type), new Code(code)); }
-
- public static TaxId empty() { return new TaxId(Country.empty(), Type.empty(), Code.empty()); }
- public boolean isEmpty() { return country.isEmpty() && type.isEmpty() && code.isEmpty(); }
-
- // TODO(bjorncs) Remove legacy once no longer present in ZK
- public static TaxId legacy(String code) { return new TaxId(Country.empty(), Type.empty(), new Code(code)); }
- public boolean isLegacy() { return type.isEmpty() && !code.isEmpty(); }
-
- public static class Country extends StringWrapper<Country> {
- public Country(String value) {
- super(value);
- requireLength(value, "tax code country length", 0, 2);
- }
-
- public static Country empty() { return new Country(""); }
- public boolean isEmpty() { return value().isEmpty(); }
- }
-
- public static class Type extends StringWrapper<Type> {
- public Type(String value) {
- super(value);
- requireLength(value, "tax code type length", 0, 16);
- }
-
- public static Type empty() { return new Type(""); }
- public boolean isEmpty() { return value().isEmpty(); }
- }
-
- public static class Code extends StringWrapper<Code> {
- public Code(String value) {
- super(value);
- requireLength(value, "tax code value length", 0, 64);
- }
-
- public static Code empty() { return new Code(""); }
- public boolean isEmpty() { return value().isEmpty(); }
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java
deleted file mode 100644
index 47a9790c970..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/Tenant.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * A tenant in hosted Vespa.
- *
- * @author mpolden
- */
-public abstract class Tenant {
-
- private final TenantName name;
- private final Instant createdAt;
- private final LastLoginInfo lastLoginInfo;
- private final Optional<Contact> contact;
- private final Instant tenantRolesLastMaintained;
- private final List<CloudAccountInfo> cloudAccounts;
-
- Tenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<Contact> contact, Instant tenantRolesLastMaintained, List<CloudAccountInfo> cloudAccounts) {
- this.name = name;
- this.createdAt = createdAt;
- this.lastLoginInfo = lastLoginInfo;
- this.contact = contact;
- this.tenantRolesLastMaintained = tenantRolesLastMaintained;
- this.cloudAccounts = cloudAccounts;
- }
-
- /** Name of this tenant */
- public TenantName name() {
- return name;
- }
-
- /** Instant when the tenant was created */
- public Instant createdAt() {
- return createdAt;
- }
-
- /** Returns login information for this tenant */
- public LastLoginInfo lastLoginInfo() {
- return lastLoginInfo;
- }
-
- /** Contact information for this tenant */
- public Optional<Contact> contact() {
- return contact;
- }
-
- public Instant tenantRolesLastMaintained() {
- return tenantRolesLastMaintained;
- }
-
- public List<CloudAccountInfo> cloudAccounts() {
- return cloudAccounts;
- }
-
- public abstract Type type();
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Tenant tenant = (Tenant) o;
- return Objects.equals(name, tenant.name);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name);
- }
-
- public static TenantName requireName(TenantName name) {
- if ( ! name.value().matches("^(?=.{1,20}$)[a-z](-?[a-z0-9]+)*$")) {
- throw new IllegalArgumentException("New tenant or application names must start with a letter, may " +
- "contain no more than 20 characters, and may only contain lowercase " +
- "letters, digits or dashes, but no double-dashes.");
- }
- return name;
- }
-
-
- public enum Type {
-
- /** Tenant authenticated through Athenz. */
- athenz,
-
- /** Tenant authenticated through some cloud identity provider. */
- cloud,
-
- /** Tenant has been deleted. */
- deleted,
-
- }
-
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantAddress.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantAddress.java
deleted file mode 100644
index 2e9d8635761..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantAddress.java
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import java.util.Objects;
-
-/**
- * A generic address container that tries to make as few assumptions about addresses as possible.
- * Most addresses have some of these fields, but with different names (e.g. postal code vs zip code).
- *
- * When consuming data from this class, do not make any assumptions about which fields have content.
- * An address might be still valid with surprisingly little information.
- *
- * All fields are non-null, but might be empty strings.
- *
- * @author ogronnesby
- */
-public class TenantAddress {
- private final String address;
- private final String code;
- private final String city;
- private final String region;
- private final String country;
-
- TenantAddress(String address, String code, String city, String region, String country) {
- this.address = Objects.requireNonNull(address, "'address' was null");
- this.code = Objects.requireNonNull(code, "'code' was null");
- this.city = Objects.requireNonNull(city, "'city' was null");
- this.region = Objects.requireNonNull(region, "'region' was null");
- this.country = Objects.requireNonNull(country, "'country' was null");
- }
-
- public static TenantAddress empty() {
- return new TenantAddress("", "", "", "", "");
- }
-
- /** Multi-line fields that has the contents of the street address (or similar) */
- public String address() { return address; }
-
- /** The ZIP or postal code part of the address */
- public String code() { return code; }
-
- /** The city of the address */
- public String city() { return city; }
-
- /** The region part of the address - e.g. a state, county, or province */
- public String region() { return region; }
-
- /** The country part of the address. Its name, not a code */
- public String country() { return country; }
-
- public boolean isEmpty() {
- return this.equals(empty());
- }
-
- public TenantAddress withAddress(String address) {
- return new TenantAddress(address, code, city, region, country);
- }
-
- public TenantAddress withCode(String code) {
- return new TenantAddress(address, code, city, region, country);
- }
-
- public TenantAddress withCity(String city) {
- return new TenantAddress(address, code, city, region, country);
- }
-
- public TenantAddress withRegion(String region) {
- return new TenantAddress(address, code, city, region, country);
- }
-
- public TenantAddress withCountry(String country) {
- return new TenantAddress(address, code, city, region, country);
- }
-
- @Override
- public String toString() {
- return "TenantAddress{" +
- "address='" + address + '\'' +
- ", code='" + code + '\'' +
- ", city='" + city + '\'' +
- ", region='" + region + '\'' +
- ", country='" + country + '\'' +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TenantAddress that = (TenantAddress) o;
- return Objects.equals(address, that.address) && Objects.equals(code, that.code) && Objects.equals(city, that.city) && Objects.equals(region, that.region) && Objects.equals(country, that.country);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(address, code, city, region, country);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java
deleted file mode 100644
index 1db84240fe2..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantBilling.java
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import java.util.Objects;
-
-/**
- * @author smorgrav
- */
-public class TenantBilling {
-
- private final TenantContact contact;
- private final TenantAddress address;
- private final TaxId taxId;
- private final PurchaseOrder purchaseOrder;
- private final Email invoiceEmail;
- private final TermsOfServiceApproval tosApproval;
-
- public TenantBilling(TenantContact contact, TenantAddress address, TaxId taxId, PurchaseOrder purchaseOrder,
- Email invoiceEmail, TermsOfServiceApproval tosApproval) {
- this.contact = Objects.requireNonNull(contact);
- this.address = Objects.requireNonNull(address);
- this.taxId = Objects.requireNonNull(taxId);
- this.purchaseOrder = Objects.requireNonNull(purchaseOrder);
- this.invoiceEmail = Objects.requireNonNull(invoiceEmail);
- this.tosApproval = Objects.requireNonNull(tosApproval);
- }
-
- public static TenantBilling empty() {
- return new TenantBilling(TenantContact.empty(), TenantAddress.empty(), TaxId.empty(), PurchaseOrder.empty(),
- Email.empty(), TermsOfServiceApproval.empty());
- }
-
- public TenantContact contact() {
- return contact;
- }
-
- public TenantAddress address() {
- return address;
- }
-
- public TaxId getTaxId() {
- return taxId;
- }
-
- public PurchaseOrder getPurchaseOrder() {
- return purchaseOrder;
- }
-
- public Email getInvoiceEmail() {
- return invoiceEmail;
- }
-
- public TermsOfServiceApproval getToSApproval() { return tosApproval; }
-
- public TenantBilling withContact(TenantContact updatedContact) {
- return new TenantBilling(updatedContact, this.address, this.taxId, this.purchaseOrder, this.invoiceEmail, tosApproval);
- }
-
- public TenantBilling withAddress(TenantAddress updatedAddress) {
- return new TenantBilling(this.contact, updatedAddress, this.taxId, this.purchaseOrder, this.invoiceEmail, tosApproval);
- }
-
- public TenantBilling withTaxId(TaxId updatedTaxId) {
- return new TenantBilling(this.contact, this.address, updatedTaxId, this.purchaseOrder, this.invoiceEmail, tosApproval);
- }
-
- public TenantBilling withPurchaseOrder(PurchaseOrder updatedPurchaseOrder) {
- return new TenantBilling(this.contact, this.address, this.taxId, updatedPurchaseOrder, this.invoiceEmail, tosApproval);
- }
-
- public TenantBilling withInvoiceEmail(Email updatedInvoiceEmail) {
- return new TenantBilling(this.contact, this.address, this.taxId, this.purchaseOrder, updatedInvoiceEmail, tosApproval);
- }
-
- public TenantBilling withToSApproval(TermsOfServiceApproval approval) {
- return new TenantBilling(contact, address, taxId, purchaseOrder, invoiceEmail, approval);
- }
-
- public boolean isEmpty() {
- return this.equals(empty());
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TenantBilling that = (TenantBilling) o;
- return Objects.equals(contact, that.contact) && Objects.equals(address, that.address)
- && Objects.equals(taxId, that.taxId) && Objects.equals(purchaseOrder, that.purchaseOrder)
- && Objects.equals(invoiceEmail, that.invoiceEmail) && Objects.equals(tosApproval, that.tosApproval);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(contact, address, taxId, purchaseOrder, invoiceEmail, tosApproval);
- }
-
- @Override
- public String toString() {
- return "TenantBilling{" +
- "contact=" + contact +
- ", address=" + address +
- ", taxId=" + taxId +
- ", purchaseOrder=" + purchaseOrder +
- ", invoiceEmail=" + invoiceEmail +
- ", tosApproval=" + tosApproval +
- '}';
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContact.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContact.java
deleted file mode 100644
index b9898553a49..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContact.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import java.util.Objects;
-
-/**
- * @author ogronnesby
- */
-public class TenantContact {
- private final String name;
- private final Email email;
- private final String phone;
-
- private TenantContact(String name, Email email, String phone) {
- this.name = Objects.requireNonNull(name);
- this.email = Objects.requireNonNull(email);
- this.phone = Objects.requireNonNull(phone);
- }
-
- public static TenantContact from(String name, Email email, String phone) {
- return new TenantContact(name, email, phone);
- }
-
- public static TenantContact from(String name, Email email) {
- return TenantContact.from(name, email, "");
- }
-
- public static TenantContact empty() {
- return new TenantContact("", Email.empty(), "");
- }
-
- public String name() { return name; }
- public Email email() { return email; }
- public String phone() { return phone; }
-
- public TenantContact withName(String name) {
- return new TenantContact(name, email, phone);
- }
-
- public TenantContact withEmail(Email email) {
- return new TenantContact(name, email, phone);
- }
-
- public TenantContact withPhone(String phone) {
- return new TenantContact(name, email, phone);
- }
-
- @Override
- public String toString() {
- return "TenantContact{" +
- "name='" + name + '\'' +
- ", email='" + email + '\'' +
- ", phone='" + phone + '\'' +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TenantContact that = (TenantContact) o;
- return Objects.equals(name, that.name) && Objects.equals(email, that.email) && Objects.equals(phone, that.phone);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name, email, phone);
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContacts.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContacts.java
deleted file mode 100644
index 6447e83113f..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantContacts.java
+++ /dev/null
@@ -1,164 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-/**
- * Tenant contacts are targets of the notification system. Sometimes they
- * are a person with an email address, other times they are a Slack channel,
- * IRC plugin, etc.
- *
- * @author ogronnesby
- */
-public class TenantContacts {
- private final List<? extends Contact> contacts;
-
- public TenantContacts(List<? extends Contact> contacts) {
- this.contacts = List.copyOf(contacts);
- for (int i = 0; i < contacts.size(); i++) {
- for (int j = 0; j < i; j++) {
- if (contacts.get(i).equals(contacts.get(j))) {
- throw new IllegalArgumentException("Duplicate contact: " + contacts.get(i));
- }
- }
- }
- }
-
- public static TenantContacts empty() {
- return new TenantContacts(List.of());
- }
-
- public List<? extends Contact> all() {
- return contacts;
- }
-
- public <T extends Contact> List<T> ofType(Class<T> type) {
- return contacts.stream()
- .filter(type::isInstance)
- .map(type::cast)
- .toList();
- }
-
- public boolean isEmpty() {
- return contacts.isEmpty();
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TenantContacts that = (TenantContacts) o;
- return contacts.equals(that.contacts);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(contacts);
- }
-
- @Override
- public String toString() {
- return "TenantContacts{" +
- "contacts=" + contacts +
- '}';
- }
-
- public abstract static class Contact {
- private final List<Audience> audiences;
-
- public Contact(List<Audience> audiences) {
- this.audiences = List.copyOf(audiences);
- if (audiences.isEmpty()) throw new IllegalArgumentException("At least one notification activity must be enabled");
- }
-
- public List<Audience> audiences() { return audiences; }
-
- public abstract Type type();
-
- public abstract boolean equals(Object o);
- public abstract int hashCode();
- public abstract String toString();
- }
-
- public static class EmailContact extends Contact {
- private final Email email;
-
- public EmailContact(List<Audience> audiences, Email email) {
- super(audiences);
- this.email = email;
- }
-
- public Email email() { return email; }
-
- public EmailContact withEmail(Email email) {
- return new EmailContact(audiences(), email);
- }
-
- @Override
- public Type type() {
- return Type.EMAIL;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- EmailContact that = (EmailContact) o;
- return email.equals(that.email);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(email);
- }
-
- @Override
- public String toString() {
- return "email '" + email + '\'';
- }
- }
-
- public enum Type {
- EMAIL("email");
-
- private final String value;
-
- Type(String value) {
- this.value = value;
- }
-
- public String value() {
- return this.value;
- }
-
- public static Optional<Type> from(String value) {
- return Arrays.stream(Type.values()).filter(x -> x.value().equals(value)).findAny();
- }
- }
-
- public enum Audience {
- // tenant admin type updates about billing etc.
- TENANT("tenant"),
-
- // system notifications like deployment failures etc.
- NOTIFICATIONS("notifications");
-
- private final String value;
-
- Audience(String value) {
- this.value = value;
- }
-
- public String value() {
- return value;
- }
-
- public static Optional<Audience> from(String value) {
- return Arrays.stream(Audience.values()).filter((x -> x.value().equals(value))).findAny();
- }
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java
deleted file mode 100644
index 6885c6d13f1..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TenantInfo.java
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import java.util.Objects;
-
-/**
- * Tenant information beyond technical tenant id and user authorizations.
- *
- * This info is used to capture generic support information and invoiced billing information.
- *
- * All fields are non null but strings can be empty
- *
- * @author smorgrav
- */
-public class TenantInfo {
-
- private final String name;
- private final String email;
- private final String website;
-
- private final TenantContact contact;
- private final TenantAddress address;
- private final TenantBilling billingContact;
- private final TenantContacts contacts;
-
- TenantInfo(String name, String email, String website, String contactName, Email contactEmail,
- TenantAddress address, TenantBilling billingContact, TenantContacts contacts) {
- this(name, email, website, TenantContact.from(contactName, contactEmail), address, billingContact, contacts);
- }
-
- TenantInfo(String name, String email, String website, TenantContact contact, TenantAddress address, TenantBilling billing, TenantContacts contacts) {
- this.name = Objects.requireNonNull(name);
- this.email = Objects.requireNonNull(email);
- this.website = Objects.requireNonNull(website);
- this.contact = Objects.requireNonNull(contact);
- this.address = Objects.requireNonNull(address);
- this.billingContact = Objects.requireNonNull(billing);
- this.contacts = Objects.requireNonNull(contacts);
- }
-
- public static TenantInfo empty() {
- return new TenantInfo("", "", "", "", Email.empty(), TenantAddress.empty(), TenantBilling.empty(), TenantContacts.empty());
- }
-
- public String name() {
- return name;
- }
-
- public String email() {
- return email;
- }
-
- public String website() {
- return website;
- }
-
- public TenantContact contact() { return contact; }
-
- public TenantAddress address() { return address; }
-
- public TenantBilling billingContact() {
- return billingContact;
- }
-
- public TenantContacts contacts() { return contacts; }
-
- public boolean isEmpty() {
- return this.equals(empty());
- }
-
- public TenantInfo withName(String name) {
- return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
- }
-
- public TenantInfo withEmail(String email) {
- return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
- }
-
- public TenantInfo withWebsite(String website) {
- return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
- }
-
- public TenantInfo withContact(TenantContact contact) {
- return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
- }
-
- public TenantInfo withAddress(TenantAddress address) {
- return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
- }
-
- public TenantInfo withBilling(TenantBilling billingContact) {
- return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
- }
-
- public TenantInfo withContacts(TenantContacts contacts) {
- return new TenantInfo(name, email, website, contact, address, billingContact, contacts);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- TenantInfo that = (TenantInfo) o;
- return Objects.equals(name, that.name) &&
- Objects.equals(email, that.email) &&
- Objects.equals(website, that.website) &&
- Objects.equals(contact, that.contact) &&
- Objects.equals(address, that.address) &&
- Objects.equals(billingContact, that.billingContact) &&
- Objects.equals(contacts, that.contacts);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name, email, website, contact, address, billingContact, contacts);
- }
-
- @Override
- public String toString() {
- return "TenantInfo{" +
- "name='" + name + '\'' +
- ", email='" + email + '\'' +
- ", website='" + website + '\'' +
- ", contact=" + contact +
- ", address=" + address +
- ", billingContact=" + billingContact +
- ", contacts=" + contacts +
- '}';
- }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TermsOfServiceApproval.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TermsOfServiceApproval.java
deleted file mode 100644
index 61fba17c473..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/TermsOfServiceApproval.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-
-import java.time.Instant;
-import java.util.Optional;
-
-/**
- * @author bjorncs
- */
-public record TermsOfServiceApproval(Instant approvedAt, Optional<SimplePrincipal> approvedBy) {
-
- public TermsOfServiceApproval(Instant at, SimplePrincipal by) { this(at, Optional.of(by)); }
-
- public TermsOfServiceApproval(String at, String by) {
- this(at.isBlank() ? Instant.EPOCH : Instant.parse(at), by.isBlank() ? Optional.empty() : Optional.of(new SimplePrincipal(by)));
- }
-
- public TermsOfServiceApproval {
- if (approvedBy.isEmpty() && !Instant.EPOCH.equals(approvedAt))
- throw new IllegalArgumentException("Missing approver");
- }
-
- public static TermsOfServiceApproval empty() { return new TermsOfServiceApproval(Instant.EPOCH, Optional.empty()); }
-
- public boolean hasApproved() { return approvedBy.isPresent(); }
- public boolean isEmpty() { return approvedBy.isEmpty() && Instant.EPOCH.equals(approvedAt); }
-}
diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java
deleted file mode 100644
index a04d1e0f794..00000000000
--- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/tenant/package-info.java
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author mpolden
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/identifiers/IdentifierTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/identifiers/IdentifierTest.java
deleted file mode 100644
index a48301f10b2..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/identifiers/IdentifierTest.java
+++ /dev/null
@@ -1,169 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.identifiers;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-/**
- * @author smorgrav
- */
-public class IdentifierTest {
-
- @Test
- void existing_tenant_id_not_empty() {
- assertThrows(IllegalArgumentException.class, () -> {
- new TenantId("");
- });
- }
-
- @Test
- void existing_tenant_id_must_check_pattern() {
- assertThrows(IllegalArgumentException.class, () -> {
- new TenantId("`");
- });
- }
-
- @Test
- void default_not_allowed_for_tenants() {
- assertThrows(IllegalArgumentException.class, () -> {
- new TenantId("default");
- });
- }
-
- @Test
- void existing_tenant_id_must_accept_valid_id() {
- new TenantId("msbe");
- }
-
- @Test
- void existing_tenant_id_cannot_be_uppercase() {
- assertThrows(IllegalArgumentException.class, () -> {
- new TenantId("MixedCaseTenant");
- });
- }
-
- @Test
- void existing_tenant_id_cannot_contain_dots() {
- assertThrows(IllegalArgumentException.class, () -> {
- new TenantId("tenant.with.dots");
- });
- }
-
- @Test
- void new_tenant_id_cannot_contain_underscore() {
- assertThrows(IllegalArgumentException.class, () -> {
- TenantId.validate("underscore_tenant");
- });
- }
-
- @Test
- void new_tenant_id_cannot_contain_dot() {
- assertThrows(IllegalArgumentException.class, () -> {
- TenantId.validate("tenant.with.dots");
- });
- }
-
- @Test
- void new_tenant_id_cannot_contain_uppercase() {
- assertThrows(IllegalArgumentException.class, () -> {
- TenantId.validate("UppercaseTenant");
- });
- }
-
- @Test
- void new_tenant_id_cannot_start_with_dash() {
- assertThrows(IllegalArgumentException.class, () -> {
- TenantId.validate("-tenant");
- });
- }
-
- @Test
- void new_tenant_id_cannot_end_with_dash() {
- assertThrows(IllegalArgumentException.class, () -> {
- TenantId.validate("tenant-");
- });
- }
-
- @Test
- void existing_application_id_cannot_be_uppercase() {
- assertThrows(IllegalArgumentException.class, () -> {
- new ApplicationId("MixedCaseApplication");
- });
- }
-
- @Test
- void existing_application_id_cannot_contain_dots() {
- assertThrows(IllegalArgumentException.class, () -> {
- new ApplicationId("application.with.dots");
- });
- }
-
- @Test
- void new_application_id_cannot_contain_underscore() {
- assertThrows(IllegalArgumentException.class, () -> {
- ApplicationId.validate("underscore_application");
- });
- }
-
- @Test
- void new_application_id_cannot_contain_dot() {
- assertThrows(IllegalArgumentException.class, () -> {
- ApplicationId.validate("application.with.dots");
- });
- }
-
- @Test
- void new_application_id_cannot_contain_uppercase() {
- assertThrows(IllegalArgumentException.class, () -> {
- ApplicationId.validate("UppercaseApplication");
- });
- }
-
- @Test
- void new_application_id_cannot_start_with_dash() {
- assertThrows(IllegalArgumentException.class, () -> {
- ApplicationId.validate("-application");
- });
- }
-
- @Test
- void new_application_id_cannot_end_with_dash() {
- assertThrows(IllegalArgumentException.class, () -> {
- ApplicationId.validate("application-");
- });
- }
-
- @Test
- void instance_id_cannot_be_uppercase() {
- assertThrows(IllegalArgumentException.class, () -> {
- new InstanceId("MixedCaseInstance");
- });
- }
-
- @Test
- void dns_names_has_no_underscore() {
- assertEquals("a-b-c", new ApplicationId("a_b_c").toDns());
- }
-
- @Test
- void identifiers_cannot_be_named_api() {
- assertThrows(IllegalArgumentException.class, () -> {
- new ApplicationId("api");
- });
- }
-
- @Test
- void application_instance_id_dotted_string_is_subindentifers_concatinated_with_dots() {
- DeploymentId id = new DeploymentId(com.yahoo.config.provision.ApplicationId.from("tenant", "application", "instance"),
- ZoneId.from("prod", "region"));
- assertEquals("tenant.application.prod.region.instance", id.dottedString());
- }
-
- @Test
- void revision_id_can_contain_application_version_number() {
- new RevisionId("1.0.1078-24825d1f6");
- }
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrlsTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrlsTest.java
deleted file mode 100644
index 259a279671b..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/ConsoleUrlsTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author freva
- */
-class ConsoleUrlsTest {
-
- private final ConsoleUrls urls = new ConsoleUrls(URI.create("https://console.tld/"));
-
- @Test
- void urls() {
- ApplicationId app = ApplicationId.from("t1", "a1", "i1");
- ZoneId prod = ZoneId.from("prod", "us-north-1");
- ZoneId dev = ZoneId.from("dev", "eu-west-2");
- ZoneId test = ZoneId.from("test", "ap-east-3");
- ClusterSpec.Id cluster = ClusterSpec.Id.from("c1");
-
- assertEquals("https://console.tld", urls.root());
- assertEquals("https://console.tld/tenant/t1", urls.tenantOverview(app.tenant()));
- assertEquals("https://console.tld/tenant/t1/account/notifications", urls.tenantNotifications(app.tenant()));
- assertEquals("https://console.tld/tenant/t1/account/billing", urls.tenantBilling(app.tenant()));
- assertEquals("https://console.tld/tenant/t1/application/a1/prod/instance", urls.prodApplicationOverview(app.tenant(), app.application()));
- assertEquals("https://console.tld/tenant/t1/application/a1/prod/instance/i1", urls.instanceOverview(app, Environment.test));
- assertEquals("https://console.tld/tenant/t1/application/a1/dev/instance/i1?i1.dev.eu-west-2=clusters%2Cc1", urls.clusterOverview(app, dev, cluster));
- assertEquals("https://console.tld/tenant/t1/application/a1/prod/instance/i1?i1.prod.us-north-1=clusters%2Cc1%3Dreindexing", urls.clusterReindexing(app, prod, cluster));
- assertEquals("https://console.tld/tenant/t1/application/a1/prod/instance/i1/job/production-us-north-1/run/1", urls.deploymentRun(new RunId(app, JobType.deploymentTo(prod), 1)));
- assertEquals("https://console.tld/tenant/t1/application/a1/prod/instance/i1/job/system-test/run/1", urls.deploymentRun(new RunId(app, JobType.deploymentTo(test), 1)));
- assertEquals("https://console.tld/tenant/t1/application/a1/dev/instance/i1/job/dev-eu-west-2/run/1", urls.deploymentRun(new RunId(app, JobType.deploymentTo(dev), 1)));
- assertEquals("https://console.tld/verify?code=test123", urls.verifyEmail("test123"));
- }
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatusTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatusTest.java
deleted file mode 100644
index 022474406b9..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/billing/BillStatusTest.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author gjoranv
- */
-public class BillStatusTest {
-
- @Test
- void legacy_states_are_converted() {
- assertEquals(BillStatus.OPEN, BillStatus.from("ISSUED"));
- assertEquals(BillStatus.OPEN, BillStatus.from("EXPORTED"));
- assertEquals(BillStatus.VOID, BillStatus.from("CANCELED"));
- }
-
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistoryTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistoryTest.java
deleted file mode 100644
index 8318a0449ea..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/billing/StatusHistoryTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package com.yahoo.vespa.hosted.controller.api.integration.billing;
-
-import org.junit.jupiter.api.Test;
-
-import java.time.Clock;
-import java.time.ZonedDateTime;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-/**
- * @author gjoranv
- */
-public class StatusHistoryTest {
-
- private final Clock clock = Clock.systemUTC();
-
- @Test
- void open_can_change_to_any_status() {
- var history = StatusHistory.open(clock);
- history.checkValidTransition(BillStatus.FROZEN);
- history.checkValidTransition(BillStatus.SUCCESSFUL);
- history.checkValidTransition(BillStatus.VOID);
- }
-
- @Test
- void frozen_cannot_change_to_open() {
- var history = new StatusHistory(historyWith(BillStatus.FROZEN));
-
- history.checkValidTransition(BillStatus.SUCCESSFUL);
- history.checkValidTransition(BillStatus.VOID);
-
- assertThrows(IllegalArgumentException.class, () -> history.checkValidTransition(BillStatus.OPEN));
- }
-
- @Test
- void closed_cannot_change() {
- var history = new StatusHistory(historyWith(BillStatus.SUCCESSFUL));
-
- assertThrows(IllegalArgumentException.class, () -> history.checkValidTransition(BillStatus.OPEN));
- assertThrows(IllegalArgumentException.class, () -> history.checkValidTransition(BillStatus.FROZEN));
- assertThrows(IllegalArgumentException.class, () -> history.checkValidTransition(BillStatus.VOID));
- }
-
- @Test
- void void_cannot_change() {
- var history = new StatusHistory(historyWith(BillStatus.VOID));
-
- assertThrows(IllegalArgumentException.class, () -> history.checkValidTransition(BillStatus.OPEN));
- assertThrows(IllegalArgumentException.class, () -> history.checkValidTransition(BillStatus.FROZEN));
- assertThrows(IllegalArgumentException.class, () -> history.checkValidTransition(BillStatus.SUCCESSFUL));
- }
-
- @Test
- void any_status_can_change_to_itself() {
- var history = new StatusHistory(historyWith(BillStatus.OPEN));
- history.checkValidTransition(BillStatus.OPEN);
-
- history = new StatusHistory(historyWith(BillStatus.FROZEN));
- history.checkValidTransition(BillStatus.FROZEN);
-
- history = new StatusHistory(historyWith(BillStatus.SUCCESSFUL));
- history.checkValidTransition(BillStatus.SUCCESSFUL);
-
- history = new StatusHistory(historyWith(BillStatus.VOID));
- history.checkValidTransition(BillStatus.VOID);
- }
-
- @Test
- void it_validates_status_history_in_constructor() {
- assertThrows(IllegalArgumentException.class, () -> new StatusHistory(historyWith(BillStatus.FROZEN, BillStatus.OPEN)));
- assertThrows(IllegalArgumentException.class, () -> new StatusHistory(historyWith(BillStatus.SUCCESSFUL, BillStatus.OPEN)));
- assertThrows(IllegalArgumentException.class, () -> new StatusHistory(historyWith(BillStatus.SUCCESSFUL, BillStatus.FROZEN)));
- assertThrows(IllegalArgumentException.class, () -> new StatusHistory(historyWith(BillStatus.SUCCESSFUL, BillStatus.VOID)));
- assertThrows(IllegalArgumentException.class, () -> new StatusHistory(historyWith(BillStatus.VOID, BillStatus.OPEN)));
- assertThrows(IllegalArgumentException.class, () -> new StatusHistory(historyWith(BillStatus.VOID, BillStatus.FROZEN)));
- assertThrows(IllegalArgumentException.class, () -> new StatusHistory(historyWith(BillStatus.VOID, BillStatus.SUCCESSFUL)));
- }
-
- private SortedMap<ZonedDateTime, BillStatus> historyWith(BillStatus... statuses) {
- var history = new TreeMap<>(Map.of(ZonedDateTime.now(clock), BillStatus.OPEN));
- for (var status : statuses) {
- history.put(ZonedDateTime.now(clock), status);
- }
- return history;
- }
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateTest.java
deleted file mode 100644
index e165157dac2..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/certificates/EndpointCertificateTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package com.yahoo.vespa.hosted.controller.api.integration.certificates;
-
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-class EndpointCertificateTest {
-
- @Test
- public void san_matches() {
- List<String> sans = List.of("*.a.example.com", "b.example.com", "c.example.com");
- assertTrue(EndpointCertificate.sanMatches("b.example.com", sans));
- assertTrue(EndpointCertificate.sanMatches("c.example.com", sans));
- assertTrue(EndpointCertificate.sanMatches("foo.a.example.com", sans));
- assertFalse(EndpointCertificate.sanMatches("", List.of()));
- assertFalse(EndpointCertificate.sanMatches("example.com", List.of()));
- assertFalse(EndpointCertificate.sanMatches("example.com", sans));
- assertFalse(EndpointCertificate.sanMatches("d.example.com", sans));
- assertFalse(EndpointCertificate.sanMatches("a.example.com", sans));
- assertFalse(EndpointCertificate.sanMatches("aa.example.com", sans));
- assertFalse(EndpointCertificate.sanMatches("c.c.example.com", sans));
- assertFalse(EndpointCertificate.sanMatches("a.a.a.example.com", sans));
- }
-
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobTypeTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobTypeTest.java
deleted file mode 100644
index 552b313e454..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/deployment/JobTypeTest.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.deployment;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author jonmv
- */
-public class JobTypeTest {
-
- @Test
- void test() {
- assertEquals(JobType.test("us-east-3"), JobType.ofSerialized("prod.us-east-3.test"));
- assertEquals(JobType.dev("aws-us-east-1c"), JobType.ofSerialized("dev.aws-us-east-1c"));
-
- assertEquals(JobType.fromJobName("production-my-zone", null), JobType.prod("my-zone"));
- assertEquals(JobType.fromJobName("test-my-zone", null), JobType.test("my-zone"));
- assertEquals(JobType.fromJobName("dev-my-zone", null), JobType.dev("my-zone"));
- assertEquals(JobType.fromJobName("perf-my-zone", null), JobType.perf("my-zone"));
-
- assertFalse(JobType.dev("snohetta").isTest());
- assertTrue(JobType.dev("snohetta").isDeployment());
- assertFalse(JobType.dev("snohetta").isProduction());
-
- assertFalse(JobType.perf("snohetta").isTest());
- assertTrue(JobType.perf("snohetta").isDeployment());
- assertFalse(JobType.perf("snohetta").isProduction());
-
- assertTrue(JobType.deploymentTo(ZoneId.from("test", "snohetta")).isTest());
- assertTrue(JobType.deploymentTo(ZoneId.from("test", "snohetta")).isDeployment());
- assertFalse(JobType.deploymentTo(ZoneId.from("test", "snohetta")).isProduction());
-
- assertTrue(JobType.deploymentTo(ZoneId.from("staging", "snohetta")).isTest());
- assertTrue(JobType.deploymentTo(ZoneId.from("staging", "snohetta")).isDeployment());
- assertFalse(JobType.deploymentTo(ZoneId.from("staging", "snohetta")).isProduction());
-
- assertFalse(JobType.prod("snohetta").isTest());
- assertTrue(JobType.prod("snohetta").isDeployment());
- assertTrue(JobType.prod("snohetta").isProduction());
-
- assertTrue(JobType.test("snohetta").isTest());
- assertFalse(JobType.test("snohetta").isDeployment());
- assertTrue(JobType.test("snohetta").isProduction());
- }
-
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTargetTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTargetTest.java
deleted file mode 100644
index 2ef254a7f2d..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/AliasTargetTest.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author mpolden
- */
-public class AliasTargetTest {
-
- @Test
- void packing() {
- List<AliasTarget> tests = List.of(
- new LatencyAliasTarget(HostName.of("foo.example.com"), "dns-zone-1", ZoneId.from("prod.us-north-1")),
- new WeightedAliasTarget(HostName.of("bar.example.com"), "dns-zone-2", "prod.us-north-2", 50)
- );
- for (var target : tests) {
- AliasTarget unpacked = AliasTarget.unpack(target.pack());
- assertEquals(target, unpacked);
- }
-
- List<RecordData> invalidData = List.of(RecordData.from(""), RecordData.from("foobar"));
- for (var data : invalidData) {
- try {
- AliasTarget.unpack(data);
- fail("Expected exception");
- } catch (IllegalArgumentException ignored) {
- }
- }
- }
-
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java
deleted file mode 100644
index f6414113d4e..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/dns/DirectTargetTest.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.dns;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author freva
- */
-class DirectTargetTest {
-
- @Test
- void packing() {
- List<DirectTarget> tests = List.of(
- new LatencyDirectTarget(RecordData.from("foo.example.com"), ZoneId.from("prod.us-north-1")),
- new WeightedDirectTarget(RecordData.from("bar.example.com"), ZoneId.from("prod.us-north-2"), 50));
- for (var target : tests) {
- DirectTarget unpacked = DirectTarget.unpack(target.pack());
- assertEquals(target, unpacked);
- }
-
- List<RecordData> invalidData = List.of(RecordData.from(""), RecordData.from("foobar"));
- for (var data : invalidData) {
- try {
- DirectTarget.unpack(data);
- fail("Expected exception");
- } catch (IllegalArgumentException ignored) { }
- }
- }
-
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/maven/MetadataTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/maven/MetadataTest.java
deleted file mode 100644
index e78e9fb64d9..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/maven/MetadataTest.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.maven;
-
-import com.yahoo.collections.Iterables;
-import com.yahoo.component.Version;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class MetadataTest {
-
- @Test
- void testParsing() {
- Metadata metadata = Metadata.fromXml(metadataXml);
- assertEquals("com.yahoo.vespa", metadata.id().groupId());
- assertEquals("tenant-base", metadata.id().artifactId());
- assertEquals(Instant.parse("2019-06-19T05:42:45.00Z"), metadata.lastUpdated());
- assertEquals(Version.fromString("6.297.80"), metadata.versions(metadata.lastUpdated()).get(0));
- assertEquals(Version.fromString("7.61.10"), Iterables.reversed(metadata.versions(metadata.lastUpdated().plusSeconds(10801)))
- .iterator().next());
- assertEquals(Version.fromString("7.60.51"), Iterables.reversed(metadata.versions(metadata.lastUpdated().plusSeconds(10800)))
- .iterator().next());
- }
-
- private static final String metadataXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
- "<metadata>\n" +
- " <groupId>com.yahoo.vespa</groupId>\n" +
- " <artifactId>tenant-base</artifactId>\n" +
- " <versioning>\n" +
- " <latest>7.61.10</latest>\n" +
- " <release>7.61.10</release>\n" +
- " <versions>\n" +
- " <version>6.297.80</version>\n" +
- " <version>6.300.15</version>\n" +
- " <version>6.301.8</version>\n" +
- " <version>6.303.29</version>\n" +
- " <version>6.304.14</version>\n" +
- " <version>6.305.35</version>\n" +
- " <version>6.328.65</version>\n" +
- " <version>6.329.64</version>\n" +
- " <version>6.330.51</version>\n" +
- " <version>7.3.19</version>\n" +
- " <version>7.18.17</version>\n" +
- " <version>7.20.129</version>\n" +
- " <version>7.21.18</version>\n" +
- " <version>7.22.18</version>\n" +
- " <version>7.38.38</version>\n" +
- " <version>7.39.5</version>\n" +
- " <version>7.40.41</version>\n" +
- " <version>7.41.15</version>\n" +
- " <version>7.57.40</version>\n" +
- " <version>7.60.51</version>\n" +
- " <version>7.61.10</version>\n" +
- " </versions>\n" +
- " <lastUpdated>20190619054245</lastUpdated>\n" +
- " </versioning>\n" +
- "</metadata>\n";
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/user/RolesTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/user/RolesTest.java
deleted file mode 100644
index f8bd0243c02..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/integration/user/RolesTest.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.integration.user;
-
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.role.ApplicationRole;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-/**
- * @author jonmv
- */
-public class RolesTest {
-
- @Test
- void testSerialization() {
- TenantName tenant = TenantName.from("my-tenant");
- for (TenantRole role : Roles.tenantRoles(tenant))
- assertEquals(role, Roles.toRole(Roles.valueOf(role)));
-
- ApplicationName application = ApplicationName.from("my-application");
- for (ApplicationRole role : Roles.applicationRoles(tenant, application))
- assertEquals(role, Roles.toRole(Roles.valueOf(role)));
-
- assertEquals(Role.hostedOperator(),
- Roles.toRole("hostedOperator"));
- assertEquals(Role.hostedSupporter(),
- Roles.toRole("hostedSupporter"));
- assertEquals(Role.administrator(tenant), Roles.toRole("my-tenant.administrator"));
- assertEquals(Role.developer(tenant), Roles.toRole("my-tenant.developer"));
- assertEquals(Role.reader(tenant), Roles.toRole("my-tenant.reader"));
- assertEquals(Role.headless(tenant, application), Roles.toRole("my-tenant.my-application.headless"));
- }
-
- @Test
- void illegalTenantName() {
- assertThrows(IllegalArgumentException.class, () -> {
- Roles.valueOf(Role.developer(TenantName.from("my.tenant")));
- });
- }
-
- @Test
- void illegalApplicationName() {
- assertThrows(IllegalArgumentException.class, () -> {
- Roles.valueOf(Role.headless(TenantName.from("my-tenant"), ApplicationName.from("my.app")));
- });
- }
-
- @Test
- void illegalRoleValue() {
- assertThrows(IllegalArgumentException.class, () -> {
- Roles.toRole("my-tenant.awesomePerson");
- });
- }
-
- @Test
- void illegalCombination() {
- assertThrows(IllegalArgumentException.class, () -> {
- Roles.toRole("my-tenant.my-application.tenantOwner");
- });
- }
-
- @Test
- void illegalValue() {
- assertThrows(IllegalArgumentException.class, () -> {
- Roles.toRole("everyone");
- });
- }
-
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/resource/ResourceSnapshotTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/resource/ResourceSnapshotTest.java
deleted file mode 100644
index a6d70b360aa..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/resource/ResourceSnapshotTest.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.resource;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceSnapshot;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.fail;
-
-public class ResourceSnapshotTest {
- @Test
- void test_adding_resources_collapse_dimensions() {
- var nodes = List.of(
- nodeWithResources(NodeResources.zero().with(NodeResources.DiskSpeed.fast)),
- nodeWithResources(NodeResources.zero().with(NodeResources.DiskSpeed.slow)));
-
- // This should be OK and not throw exception
- var snapshot = ResourceSnapshot.from(nodes, Instant.EPOCH, ZoneId.defaultId());
-
- assertEquals(NodeResources.DiskSpeed.any, snapshot.resources().diskSpeed());
- }
-
- @Test
- void test_adding_resources_fail() {
- var nodes = List.of(
- nodeWithResources(NodeResources.zero().with(NodeResources.Architecture.x86_64)),
- nodeWithResources(NodeResources.zero().with(NodeResources.Architecture.arm64)));
-
- try {
- ResourceSnapshot.from(nodes, Instant.EPOCH, ZoneId.defaultId());
- fail("Should throw an exception");
- } catch (IllegalArgumentException e) {
- // this should happen
- }
- }
-
- private Node nodeWithResources(NodeResources resources) {
- return Node.builder()
- .hostname("a")
- .owner(ApplicationId.defaultId())
- .resources(resources)
- .build();
- }
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/PathGroupTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/PathGroupTest.java
deleted file mode 100644
index 87e76b7ce09..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/PathGroupTest.java
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import org.junit.jupiter.api.Test;
-
-import java.util.HashSet;
-import java.util.Set;
-import java.util.regex.Pattern;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author jonmv
- * @author mpolden
- */
-public class PathGroupTest {
-
- @Test
- void uniqueMatches() {
- // Ensure that each path group contains at most one match for any given path, to avoid undefined context extraction.
- Set<String> checkedAgainstSelf = new HashSet<>();
- for (PathGroup group1 : PathGroup.values())
- for (PathGroup group2 : PathGroup.values())
- for (String path1 : group1.pathSpecs)
- for (String path2 : group2.pathSpecs) {
- if (path1.equals(path2)) {
- if (checkedAgainstSelf.add(path1)) continue;
- fail("Path '" + path1 + "' appears in both '" + group1 + "' and '" + group2 + "'.");
- }
-
- String[] parts1 = path1.split("/");
- String[] parts2 = path2.split("/");
-
- int end = Math.min(parts1.length, parts2.length);
- // If one path has more parts than the other ...
- // and the other doesn't end with a wildcard matcher ...
- // and the longest one isn't just one wildcard longer ...
- // then one is strictly longer than the other, and it's not a match.
- if (end < parts1.length && (end == 0 || !parts2[end - 1].equals("{*}")) && !parts1[end].equals("{*}")) continue;
- if (end < parts2.length && (end == 0 || !parts1[end - 1].equals("{*}")) && !parts2[end].equals("{*}")) continue;
-
- int i;
- for (i = 0; i < end; i++)
- if ( ! parts1[i].equals(parts2[i])
- && !(parts1[i].startsWith("{") && parts1[i].endsWith("}"))
- && !(parts2[i].startsWith("{") && parts2[i].endsWith("}"))) break;
-
- if (i == end) fail("Paths '" + path1 + "' and '" + path2 + "' overlap.");
- }
-
- assertEquals(PathGroup.all().stream().mapToInt(group -> group.pathSpecs.size()).sum(),
- checkedAgainstSelf.size());
- }
-
- @Test
- void contextMatches() {
- for (PathGroup group : PathGroup.values())
- for (String spec : group.pathSpecs) {
- for (PathGroup.Matcher matcher : PathGroup.Matcher.values()) {
- if (group.matchers.contains(matcher)) {
- if (!spec.contains(matcher.pattern))
- fail("Spec '" + spec + "' in '" + group.name() + "' should contain matcher '" + matcher.pattern + "'.");
- if (spec.replaceFirst(Pattern.quote(matcher.pattern), "").contains(matcher.pattern))
- fail("Spec '" + spec + "' in '" + group.name() + "' contains more than one instance of '" + matcher.pattern + "'.");
- }
- else if (spec.contains(matcher.pattern))
- fail("Spec '" + spec + "' in '" + group.name() + "' should not contain matcher '" + matcher.pattern + "'.");
- }
- }
- }
-
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java
deleted file mode 100644
index c8020666906..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/role/RoleTest.java
+++ /dev/null
@@ -1,181 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.role;
-
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-public class RoleTest {
-
- private static final Enforcer mainEnforcer = new Enforcer(SystemName.main);
- private static final Enforcer publicEnforcer = new Enforcer(SystemName.Public);
- private static final Enforcer publicCdEnforcer = new Enforcer(SystemName.PublicCd);
-
- @Test
- void operator_membership() {
- Role role = Role.hostedOperator();
-
- // Operator actions
- assertFalse(mainEnforcer.allows(role, Action.create, URI.create("/not/explicitly/defined")));
- assertTrue(mainEnforcer.allows(role, Action.create, URI.create("/controller/v1/foo")));
- assertTrue(mainEnforcer.allows(role, Action.update, URI.create("/os/v1/bar")));
- assertTrue(mainEnforcer.allows(role, Action.update, URI.create("/application/v4/tenant/t1/application/a1")));
- assertTrue(mainEnforcer.allows(role, Action.update, URI.create("/application/v4/tenant/t2/application/a2")));
- assertTrue(mainEnforcer.allows(role, Action.read, URI.create("/routing/v1/")));
- assertTrue(mainEnforcer.allows(role, Action.read, URI.create("/routing/v1/status/environment/")));
- assertTrue(mainEnforcer.allows(role, Action.read, URI.create("/routing/v1/status/environment/prod")));
- assertTrue(mainEnforcer.allows(role, Action.create, URI.create("/routing/v1/inactive/environment/prod/region/us-north-1")));
- }
-
- @Test
- void supporter_membership() {
- Role role = Role.hostedSupporter();
-
- // No create update or delete
- assertFalse(mainEnforcer.allows(role, Action.create, URI.create("/not/explicitly/defined")));
- assertFalse(mainEnforcer.allows(role, Action.create, URI.create("/controller/v1/foo")));
- assertFalse(mainEnforcer.allows(role, Action.update, URI.create("/os/v1/bar")));
- assertFalse(mainEnforcer.allows(role, Action.update, URI.create("/application/v4/tenant/t1/application/a1")));
- assertFalse(mainEnforcer.allows(role, Action.update, URI.create("/application/v4/tenant/t2/application/a2")));
- assertFalse(mainEnforcer.allows(role, Action.delete, URI.create("/application/v4/tenant/t8/application/a6/instance/i1/environment/dev/region/r1")));
-
- // But reads is ok (but still only for valid paths)
- assertFalse(mainEnforcer.allows(role, Action.read, URI.create("/not/explicitly/defined")));
- assertTrue(mainEnforcer.allows(role, Action.read, URI.create("/controller/v1/foo")));
- assertTrue(mainEnforcer.allows(role, Action.read, URI.create("/os/v1/bar")));
- assertTrue(mainEnforcer.allows(role, Action.read, URI.create("/application/v4/tenant/t1/application/a1")));
- assertTrue(mainEnforcer.allows(role, Action.read, URI.create("/application/v4/tenant/t2/application/a2")));
- assertFalse(mainEnforcer.allows(role, Action.delete, URI.create("/application/v4/tenant/t8/application/a6/instance/i1/environment/dev/region/r1")));
-
- // Check that we are allowed to create tenants in public.
- // hostedSupporter isn't actually allowed to create tenants - but any logged in user will be a member of the "everyone" role.
- assertTrue(publicEnforcer.allows(Role.everyone(), Action.create, URI.create("/application/v4/tenant/t1")));
- }
-
- @Test
- void tenant_membership() {
- Role role = Role.athenzTenantAdmin(TenantName.from("t1"));
- assertFalse(mainEnforcer.allows(role, Action.create, URI.create("/not/explicitly/defined")));
- assertFalse(mainEnforcer.allows(role, Action.create, URI.create("/controller/v1/foo")), "Deny access to operator API");
- assertFalse(mainEnforcer.allows(role, Action.update, URI.create("/application/v4/tenant/t2/application/a2")), "Deny access to other tenant and app");
- assertTrue(mainEnforcer.allows(role, Action.update, URI.create("/application/v4/tenant/t1/application/a1")));
-
- Role publicSystem = Role.athenzTenantAdmin(TenantName.from("t1"));
- assertFalse(publicEnforcer.allows(publicSystem, Action.read, URI.create("/controller/v1/foo")));
- assertTrue(publicEnforcer.allows(publicSystem, Action.read, URI.create("/badge/v1/badge")));
- assertTrue(publicEnforcer.allows(publicSystem, Action.update, URI.create("/application/v4/tenant/t1/application/a1")));
- }
-
- @Test
- void build_service_membership() {
- Role role = Role.buildService(TenantName.from("t1"), ApplicationName.from("a1"));
- assertFalse(publicEnforcer.allows(role, Action.create, URI.create("/not/explicitly/defined")));
- assertFalse(publicEnforcer.allows(role, Action.update, URI.create("/application/v4/tenant/t1/application/a1")));
- assertTrue(publicEnforcer.allows(role, Action.create, URI.create("/application/v4/tenant/t1/application/a1/submit")));
- assertFalse(publicEnforcer.allows(role, Action.read, URI.create("/controller/v1/foo")), "No global read access");
- }
-
- @Test
- void new_implications() {
- TenantName tenant1 = TenantName.from("t1");
- ApplicationName application1 = ApplicationName.from("a1");
- ApplicationName application2 = ApplicationName.from("a2");
-
- Role tenantAdmin1 = Role.administrator(tenant1);
- Role tenantDeveloper1 = Role.developer(tenant1);
- Role applicationHeadless11 = Role.headless(tenant1, application1);
- Role applicationHeadless12 = Role.headless(tenant1, application2);
-
- assertFalse(tenantAdmin1.implies(tenantDeveloper1));
- assertFalse(tenantAdmin1.implies(applicationHeadless11));
- assertFalse(applicationHeadless11.implies(applicationHeadless12));
- }
-
- @Test
- void system_flags() {
- URI deployUri = URI.create("/system-flags/v1/deploy");
- Action action = Action.update;
- assertTrue(mainEnforcer.allows(Role.systemFlagsDeployer(), action, deployUri));
- assertTrue(mainEnforcer.allows(Role.hostedOperator(), action, deployUri));
- assertFalse(mainEnforcer.allows(Role.hostedSupporter(), action, deployUri));
- assertFalse(mainEnforcer.allows(Role.systemFlagsDryrunner(), action, deployUri));
- assertFalse(mainEnforcer.allows(Role.everyone(), action, deployUri));
-
- URI dryrunUri = URI.create("/system-flags/v1/dryrun");
- assertTrue(mainEnforcer.allows(Role.systemFlagsDeployer(), action, dryrunUri));
- assertTrue(mainEnforcer.allows(Role.hostedOperator(), action, dryrunUri));
- assertFalse(mainEnforcer.allows(Role.hostedSupporter(), action, dryrunUri));
- assertTrue(mainEnforcer.allows(Role.systemFlagsDryrunner(), action, dryrunUri));
- assertFalse(mainEnforcer.allows(Role.everyone(), action, dryrunUri));
- }
-
- @Test
- void routing() {
- var tenantUrl = URI.create("/routing/v1/status/tenant/t1");
- var applicationUrl = URI.create("/routing/v1/status/tenant/t1/application/a1");
- var instanceUrl = URI.create("/routing/v1/status/tenant/t1/application/a1/instance/i1");
- var deploymentUrl = URI.create("/routing/v1/status/tenant/t1/application/a1/instance/i1/environment/prod/region/us-north-1");
- // Read
- for (var url : List.of(tenantUrl, applicationUrl, instanceUrl, deploymentUrl)) {
- var allowedRole = Role.reader(TenantName.from("t1"));
- var disallowedRole = Role.reader(TenantName.from("t2"));
- assertTrue(mainEnforcer.allows(allowedRole, Action.read, url), allowedRole + " can read " + url);
- assertFalse(mainEnforcer.allows(disallowedRole, Action.read, url), disallowedRole + " cannot read " + url);
- }
-
- // Write
- {
- var url = URI.create("/routing/v1/inactive/tenant/t1/application/a1/instance/i1/environment/prod/region/us-north-1");
- var allowedRole = Role.developer(TenantName.from("t1"));
- var disallowedRole = Role.developer(TenantName.from("t2"));
- assertTrue(mainEnforcer.allows(allowedRole, Action.create, url), allowedRole + " can override status at " + url);
- assertTrue(mainEnforcer.allows(allowedRole, Action.delete, url), allowedRole + " can clear status at " + url);
- assertFalse(mainEnforcer.allows(disallowedRole, Action.create, url), disallowedRole + " cannot override status at " + url);
- assertFalse(mainEnforcer.allows(disallowedRole, Action.delete, url), disallowedRole + " cannot clear status at " + url);
- }
- }
-
- private static class EnforcerTester {
- private final Enforcer enforcer;
- private final URI resource;
-
- EnforcerTester(Enforcer enforcer) {
- this(enforcer, null);
- }
-
- EnforcerTester(Enforcer enforcer, URI uri) {
- this.enforcer = enforcer;
- this.resource = uri;
- }
-
- public EnforcerTester on(String uri) {
- return new EnforcerTester(enforcer, URI.create(uri));
- }
-
- public EnforcerTester assertAction(Role role, Action ...actions) {
- var allowed = List.of(actions);
-
- allowed.forEach(action -> {
- var msg = String.format("%s should be allowed to %s on %s", role, action, resource);
- assertTrue(enforcer.allows(role, action, resource), msg);
- });
-
- Action.all().stream().filter(a -> ! allowed.contains(a)).forEach(action -> {
- var msg = String.format("%s should not be allowed to %s on %s", role, action, resource);
- assertFalse(enforcer.allows(role, action, resource), msg);
- });
-
- return this;
- }
- }
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTargetTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTargetTest.java
deleted file mode 100644
index 10ed4a278ee..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/FlagsTargetTest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1;
-
-import com.yahoo.config.provision.SystemName;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * @author hakonhall
- */
-class FlagsTargetTest {
- @Test
- void sanityCheckFilename() {
- assertTrue(FlagsTarget.filenameForSystem("default.json", SystemName.main));
- assertTrue(FlagsTarget.filenameForSystem("main.json", SystemName.main));
- assertTrue(FlagsTarget.filenameForSystem("main.controller.json", SystemName.main));
- assertTrue(FlagsTarget.filenameForSystem("main.prod.json", SystemName.main));
- assertTrue(FlagsTarget.filenameForSystem("main.prod.us-west-1.json", SystemName.main));
- assertTrue(FlagsTarget.filenameForSystem("main.prod.abc-foo-3.json", SystemName.main));
-
- assertFalse(FlagsTarget.filenameForSystem("public.json", SystemName.main));
- assertFalse(FlagsTarget.filenameForSystem("public.controller.json", SystemName.main));
- assertFalse(FlagsTarget.filenameForSystem("public.prod.json", SystemName.main));
- assertFalse(FlagsTarget.filenameForSystem("public.prod.us-west-1.json", SystemName.main));
- assertFalse(FlagsTarget.filenameForSystem("public.prod.abc-foo-3.json", SystemName.main));
-
- assertFlagValidationException("First part of flag filename is neither 'default' nor a valid system: defaults.json", "defaults.json");
- assertFlagValidationException("Invalid flag filename: default", "default");
- assertFlagValidationException("Invalid flag filename: README", "README");
- assertFlagValidationException("First part of flag filename is neither 'default' nor a valid system: nosystem.json", "nosystem.json");
- assertFlagValidationException("Invalid environment in flag filename: main.noenv.json", "main.noenv.json");
- assertFlagValidationException("Invalid region in flag filename: main.prod.%badregion.json", "main.prod.%badregion.json");
- }
-
- private void assertFlagValidationException(String expectedMessage, String filename) {
- FlagValidationException e = assertThrows(FlagValidationException.class, () -> FlagsTarget.filenameForSystem(filename, SystemName.main));
- assertEquals(expectedMessage, e.getMessage());
- }
-
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java
deleted file mode 100644
index df487f789cf..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/api/systemflags/v1/SystemFlagsDataArchiveTest.java
+++ /dev/null
@@ -1,487 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.api.systemflags.v1;
-
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.config.provision.zone.ZoneList;
-import com.yahoo.text.JSON;
-import com.yahoo.vespa.athenz.api.AthenzService;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagId;
-import com.yahoo.vespa.flags.RawFlag;
-import com.yahoo.vespa.flags.json.Condition;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URI;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Stream;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-/**
- * @author bjorncs
- */
-public class SystemFlagsDataArchiveTest {
-
- private static final SystemName SYSTEM = SystemName.main;
- private static final FlagId MY_TEST_FLAG = new FlagId("my-test-flag");
- private static final FlagId FLAG_WITH_EMPTY_DATA = new FlagId("flag-with-empty-data");
-
- @TempDir
- public File temporaryFolder;
-
- private static final FlagsTarget mainControllerTarget = createControllerTarget(SYSTEM);
- private static final FlagsTarget cdControllerTarget = createControllerTarget(SystemName.cd);
- private static final FlagsTarget prodUsWestCfgTarget = createConfigserverTarget(Environment.prod, "us-west-1");
- private static final FlagsTarget prodUsEast3CfgTarget = createConfigserverTarget(Environment.prod, "us-east-3");
- private static final FlagsTarget devUsEast1CfgTarget = createConfigserverTarget(Environment.dev, "us-east-1");
-
- private static FlagsTarget createControllerTarget(SystemName system) {
- return new ControllerFlagsTarget(system, CloudName.YAHOO, ZoneId.from(Environment.prod, RegionName.from("us-east-1")));
- }
-
- private static FlagsTarget createConfigserverTarget(Environment environment, String region) {
- return new ConfigServerFlagsTarget(
- SYSTEM,
- CloudName.YAHOO,
- ZoneId.from(environment, RegionName.from(region)),
- URI.create("https://cfg-" + region),
- new AthenzService("vespa.cfg-" + region));
- }
-
- @Test
- void can_serialize_and_deserialize_archive() throws IOException {
- can_serialize_and_deserialize_archive(false);
- can_serialize_and_deserialize_archive(true);
- }
-
- private void can_serialize_and_deserialize_archive(boolean simulateInController) throws IOException {
- File tempFile = File.createTempFile("serialized-flags-archive", null, temporaryFolder);
- try (OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile))) {
- var archive = fromDirectory("system-flags", simulateInController);
- if (simulateInController)
- archive.validateAllFilesAreForTargets(Set.of(mainControllerTarget, prodUsWestCfgTarget));
- archive.toZip(out);
- }
- try (InputStream in = new BufferedInputStream(new FileInputStream(tempFile))) {
- SystemFlagsDataArchive archive = SystemFlagsDataArchive.fromZip(in, createZoneRegistryMock());
- assertArchiveReturnsCorrectTestFlagDataForTarget(archive);
- }
- }
-
- @Test
- void retrieves_correct_flag_data_for_target() {
- retrieves_correct_flag_data_for_target(false);
- retrieves_correct_flag_data_for_target(true);
- }
-
- private void retrieves_correct_flag_data_for_target(boolean simulateInController) {
- var archive = fromDirectory("system-flags", simulateInController);
- if (simulateInController)
- archive.validateAllFilesAreForTargets(Set.of(mainControllerTarget, prodUsWestCfgTarget));
- assertArchiveReturnsCorrectTestFlagDataForTarget(archive);
- }
-
- @Test
- void supports_multi_level_flags_directory() {
- supports_multi_level_flags_directory(false);
- supports_multi_level_flags_directory(true);
- }
-
- private void supports_multi_level_flags_directory(boolean simulateInController) {
- var archive = fromDirectory("system-flags-multi-level", simulateInController);
- if (simulateInController)
- archive.validateAllFilesAreForTargets(Set.of(mainControllerTarget, prodUsWestCfgTarget));
- assertFlagDataHasValue(archive, MY_TEST_FLAG, mainControllerTarget, "default");
- }
-
- @Test
- void duplicated_flagdata_is_detected() {
- duplicated_flagdata_is_detected(false);
- duplicated_flagdata_is_detected(true);
- }
-
- private void duplicated_flagdata_is_detected(boolean simulateInController) {
- Throwable exception = assertThrows(FlagValidationException.class, () -> {
- fromDirectory("system-flags-multi-level-with-duplicated-flagdata", simulateInController);
- });
- assertTrue(exception.getMessage().contains("contains redundant flag data for id 'my-test-flag' already set in another directory!"));
- }
-
- @Test
- void empty_files_are_handled_as_no_flag_data_for_target() {
- empty_files_are_handled_as_no_flag_data_for_target(false);
- empty_files_are_handled_as_no_flag_data_for_target(true);
- }
-
- private void empty_files_are_handled_as_no_flag_data_for_target(boolean simulateInController) {
- var archive = fromDirectory("system-flags", simulateInController);
- if (simulateInController)
- archive.validateAllFilesAreForTargets(Set.of(mainControllerTarget, prodUsWestCfgTarget));
- assertNoFlagData(archive, FLAG_WITH_EMPTY_DATA, mainControllerTarget);
- assertFlagDataHasValue(archive, FLAG_WITH_EMPTY_DATA, prodUsWestCfgTarget, "main.prod.us-west-1");
- assertNoFlagData(archive, FLAG_WITH_EMPTY_DATA, prodUsEast3CfgTarget);
- assertFlagDataHasValue(archive, FLAG_WITH_EMPTY_DATA, devUsEast1CfgTarget, "main");
- }
-
- @Test
- void hv_throws_exception_on_non_json_file() {
- Throwable exception = assertThrows(FlagValidationException.class, () -> {
- fromDirectory("system-flags-with-invalid-file-name", false);
- });
- assertEquals("Invalid flag filename: file-name-without-dot-json",
- exception.getMessage());
- }
-
- @Test
- void throws_exception_on_unknown_file() {
- Throwable exception = assertThrows(FlagValidationException.class, () -> {
- SystemFlagsDataArchive archive = fromDirectory("system-flags-with-unknown-file-name", true);
- archive.validateAllFilesAreForTargets(Set.of(mainControllerTarget, prodUsWestCfgTarget));
- });
- assertEquals("Unknown flag file: flags/my-test-flag/main.prod.unknown-region.json", exception.getMessage());
- }
-
- @Test
- void unknown_region_is_still_zipped() {
- // This is useful when the program zipping the files is on a different version than the controller
- var archive = fromDirectory("system-flags-with-unknown-file-name", false);
- assertTrue(archive.hasFlagData(MY_TEST_FLAG, "main.prod.unknown-region.json"));
- }
-
- @Test
- void throws_exception_on_unknown_region() {
- Throwable exception = assertThrows(FlagValidationException.class, () -> {
- var archive = fromDirectory("system-flags-with-unknown-file-name", true);
- archive.validateAllFilesAreForTargets(Set.of(mainControllerTarget, prodUsWestCfgTarget));
- });
- assertEquals("Unknown flag file: flags/my-test-flag/main.prod.unknown-region.json", exception.getMessage());
- }
-
- @Test
- void throws_on_unknown_field() {
- Throwable exception = assertThrows(FlagValidationException.class, () -> {
- fromDirectory("system-flags-with-unknown-field-name", true);
- });
- assertEquals("""
- In file flags/my-test-flag/main.prod.us-west-1.json: Unknown non-comment fields or rules with null values: after removing any comment fields the JSON is:
- {"id":"my-test-flag","rules":[{"condition":[{"type":"whitelist","dimension":"hostname","values":["foo.com"]}],"value":"default"}]}
- but deserializing this ended up with:
- {"id":"my-test-flag","rules":[{"value":"default"}]}
- These fields may be spelled wrong, or remove them?
- See https://git.ouroath.com/vespa/hosted-feature-flags for more info on the JSON syntax
- """,
- exception.getMessage());
- }
-
- @Test
- void handles_absent_rule_value() {
- SystemFlagsDataArchive archive = fromDirectory("system-flags-with-null-value", true);
-
- // west has null value on first rule
- List<FlagData> westFlagData = archive.flagData(prodUsWestCfgTarget);
- assertEquals(1, westFlagData.size());
- assertEquals(2, westFlagData.get(0).rules().size());
- assertEquals(Optional.empty(), westFlagData.get(0).rules().get(0).getValueToApply());
-
- // east has no value on first rule
- List<FlagData> eastFlagData = archive.flagData(prodUsEast3CfgTarget);
- assertEquals(1, eastFlagData.size());
- assertEquals(2, eastFlagData.get(0).rules().size());
- assertEquals(Optional.empty(), eastFlagData.get(0).rules().get(0).getValueToApply());
- }
-
- @Test
- void remove_comments_and_null_value_in_rules() {
- assertTrue(JSON.equals("""
- {
- "id": "foo",
- "rules": [
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "hostname",
- "values": [ "foo.com" ]
- }
- ]
- },
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "zone",
- "values": [ "prod.us-west-1" ]
- }
- ]
- },
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "instance",
- "values": [ "f:o:o" ]
- }
- ],
- "value": true
- },
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "application",
- "values": [ "f:o" ]
- }
- ],
- "value": true
- }
- ]
- }""",
- normalizeJson("""
- {
- "id": "foo",
- "comment": "bar",
- "rules": [
- {
- "comment": "bar",
- "conditions": [
- {
- "comment": "bar",
- "type": "whitelist",
- "dimension": "hostname",
- "values": [ "foo.com" ]
- }
- ],
- "value": null
- },
- {
- "comment": "bar",
- "conditions": [
- {
- "comment": "bar",
- "type": "whitelist",
- "dimension": "zone",
- "values": [ "prod.us-west-1" ]
- }
- ]
- },
- {
- "comment": "bar",
- "conditions": [
- {
- "comment": "bar",
- "type": "whitelist",
- "dimension": "instance",
- "values": [ "f:o:o" ]
- }
- ],
- "value": true
- },
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "application",
- "values": [ "f:o" ]
- }
- ],
- "value": true
- }
- ]
- }""")));
- }
-
- private static String normalizeJson(String json) {
- SystemFlagsDataArchive.Builder builder = new SystemFlagsDataArchive.Builder();
- assertTrue(builder.maybeAddFile(Path.of("flags/temporary/foo/default.json"), json, createZoneRegistryMock(), true));
- List<FlagData> flagData = builder.build().flagData(prodUsWestCfgTarget);
- assertEquals(1, flagData.size());
- return JSON.canonical(flagData.get(0).serializeToJson());
- }
-
- @Test
- void normalize_json_succeed_on_valid_values() {
- addFile(Condition.Type.WHITELIST, "application", "a:b");
- addFile(Condition.Type.WHITELIST, "cloud", "yahoo");
- addFile(Condition.Type.WHITELIST, "cloud", "aws");
- addFile(Condition.Type.WHITELIST, "cloud", "gcp");
- addFile(Condition.Type.WHITELIST, "cluster-id", "some-id");
- addFile(Condition.Type.WHITELIST, "cluster-type", "admin");
- addFile(Condition.Type.WHITELIST, "cluster-type", "container");
- addFile(Condition.Type.WHITELIST, "cluster-type", "content");
- addFile(Condition.Type.WHITELIST, "console-user-email", "name@domain.com");
- addFile(Condition.Type.WHITELIST, "environment", "prod");
- addFile(Condition.Type.WHITELIST, "environment", "staging");
- addFile(Condition.Type.WHITELIST, "environment", "test");
- addFile(Condition.Type.WHITELIST, "hostname", "2080046-v6-11.ostk.bm2.prod.gq1.yahoo.com");
- addFile(Condition.Type.WHITELIST, "instance", "a:b:c");
- addFile(Condition.Type.WHITELIST, "node-type", "tenant");
- addFile(Condition.Type.WHITELIST, "node-type", "host");
- addFile(Condition.Type.WHITELIST, "node-type", "config");
- addFile(Condition.Type.WHITELIST, "node-type", "host");
- addFile(Condition.Type.WHITELIST, "system", "main");
- addFile(Condition.Type.WHITELIST, "system", "public");
- addFile(Condition.Type.WHITELIST, "tenant", "vespa");
- addFile(Condition.Type.RELATIONAL, "vespa-version", ">=8.201.13");
- addFile(Condition.Type.WHITELIST, "zone", "prod.us-west-1");
- }
-
- private void addFile(Condition.Type type, String dimension, String jsonValue) {
- SystemFlagsDataArchive.Builder builder = new SystemFlagsDataArchive.Builder();
-
- String valuesField = type == Condition.Type.RELATIONAL ?
- "\"predicate\": \"%s\"".formatted(jsonValue) :
- "\"values\": [ \"%s\" ]".formatted(jsonValue);
-
- assertTrue(builder.maybeAddFile(Path.of("flags/temporary/foo/default.json"), """
- {
- "id": "foo",
- "rules": [
- {
- "conditions": [
- {
- "type": "%s",
- "dimension": "%s",
- %s
- }
- ],
- "value": true
- }
- ]
- }
- """.formatted(type.toWire(), dimension, valuesField),
- createZoneRegistryMock(),
- true));
- }
-
- @Test
- void normalize_json_fail_on_invalid_values() {
- failAddFile(Condition.Type.WHITELIST, "application", "a.b", "In file flags/temporary/foo/default.json: Invalid application 'a.b' in whitelist condition: Applications must be on the form tenant:application, but was a.b");
- failAddFile(Condition.Type.WHITELIST, "cloud", "foo", "In file flags/temporary/foo/default.json: Unknown cloud: foo");
- // cluster-id: any String is valid
- failAddFile(Condition.Type.WHITELIST, "cluster-type", "foo", "In file flags/temporary/foo/default.json: Invalid cluster-type 'foo' in whitelist condition: Illegal cluster type 'foo'");
- failAddFile(Condition.Type.WHITELIST, "console-user-email", "not-valid-email-address", "In file flags/temporary/foo/default.json: Invalid email address: not-valid-email-address");
- failAddFile(Condition.Type.WHITELIST, "environment", "foo", "In file flags/temporary/foo/default.json: Invalid environment 'foo' in whitelist condition: 'foo' is not a valid environment identifier");
- failAddFile(Condition.Type.WHITELIST, "instance", "a.b.c", "In file flags/temporary/foo/default.json: Invalid instance 'a.b.c' in whitelist condition: Application ids must be on the form tenant:application:instance, but was a.b.c");
- failAddFile(Condition.Type.WHITELIST, "hostname", "not:a:hostname", "In file flags/temporary/foo/default.json: Invalid hostname 'not:a:hostname' in whitelist condition: hostname must match '(([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])\\.?', but got: 'not:a:hostname'");
- failAddFile(Condition.Type.WHITELIST, "node-type", "footype", "In file flags/temporary/foo/default.json: Invalid node-type 'footype' in whitelist condition: No enum constant com.yahoo.config.provision.NodeType.footype");
- failAddFile(Condition.Type.WHITELIST, "system", "bar", "In file flags/temporary/foo/default.json: Invalid system 'bar' in whitelist condition: 'bar' is not a valid system");
- failAddFile(Condition.Type.WHITELIST, "tenant", "a tenant", "In file flags/temporary/foo/default.json: Invalid tenant 'a tenant' in whitelist condition: tenant name must match '[a-zA-Z0-9_-]{1,256}', but got: 'a tenant'");
- failAddFile(Condition.Type.WHITELIST, "vespa-version", "not-a-version", "In file flags/temporary/foo/default.json: whitelist vespa-version condition is not supported");
- failAddFile(Condition.Type.RELATIONAL, "vespa-version", ">7.1.2", "In file flags/temporary/foo/default.json: Major Vespa version must be at least 8: 7.1.2");
- failAddFile(Condition.Type.WHITELIST, "zone", "dev.%illegal", "In file flags/temporary/foo/default.json: Invalid zone 'dev.%illegal' in whitelist condition: region name must match '[a-z]([a-z0-9-]*[a-z0-9])*', but got: '%illegal'");
- }
-
- private void failAddFile(Condition.Type type, String dimension, String jsonValue, String expectedExceptionMessage) {
- try {
- addFile(type, dimension, jsonValue);
- fail();
- } catch (RuntimeException e) {
- assertEquals(expectedExceptionMessage, e.getMessage());
- }
- }
-
- @Test
- void ignores_files_not_related_to_specified_system_definition() {
- var archive = fromDirectory("system-flags-for-multiple-systems", false);
- assertFlagDataHasValue(archive, MY_TEST_FLAG, cdControllerTarget, "default"); // Would be 'cd.controller' if files for CD system were included
- assertFlagDataHasValue(archive, MY_TEST_FLAG, mainControllerTarget, "default");
- assertFlagDataHasValue(archive, MY_TEST_FLAG, prodUsWestCfgTarget, "main.prod.us-west-1");
- }
-
- private SystemFlagsDataArchive fromDirectory(String testDirectory, boolean simulateInController) {
- return SystemFlagsDataArchive.fromDirectory(Paths.get("src/test/resources/" + testDirectory), createZoneRegistryMock(), simulateInController);
- }
-
- @SuppressWarnings("unchecked") // workaround for mocking a method for generic return type
- private static ZoneRegistry createZoneRegistryMock() {
- // Cannot use the standard registry mock as it's located in controller-server module
- ZoneRegistry registryMock = mock(ZoneRegistry.class);
- when(registryMock.system()).thenReturn(SystemName.main);
- ZoneApi zoneApi = mock(ZoneApi.class);
- when(zoneApi.getSystemName()).thenReturn(SystemName.main);
- when(zoneApi.getCloudName()).thenReturn(CloudName.YAHOO);
- when(zoneApi.getVirtualId()).thenReturn(ZoneId.ofVirtualControllerZone());
- when(registryMock.systemZone()).thenReturn(zoneApi);
- when(registryMock.getConfigServerVipUri(any())).thenReturn(URI.create("http://localhost:8080/"));
- when(registryMock.getConfigServerHttpsIdentity(any())).thenReturn(new AthenzService("domain", "servicename"));
- ZoneList zones = mockZoneList("prod.us-west-1", "prod.us-east-3");
- when(registryMock.zones()).thenReturn(zones);
- ZoneList zonesIncludingSystem = mockZoneList("prod.us-west-1", "prod.us-east-3", "prod.controller");
- when(registryMock.zonesIncludingSystem()).thenReturn(zonesIncludingSystem);
- return registryMock;
- }
-
- @SuppressWarnings("unchecked") // workaround for mocking a method for generic return type
- private static ZoneList mockZoneList(String... zones) {
- ZoneList zoneListMock = mock(ZoneList.class);
- when(zoneListMock.reachable()).thenReturn(zoneListMock);
- when(zoneListMock.all()).thenReturn(zoneListMock);
- List<? extends ZoneApi> zoneList = Stream.of(zones).map(SimpleZone::new).toList();
- when(zoneListMock.zones()).thenReturn((List) zoneList);
- return zoneListMock;
- }
-
- private static void assertArchiveReturnsCorrectTestFlagDataForTarget(SystemFlagsDataArchive archive) {
- assertFlagDataHasValue(archive, MY_TEST_FLAG, mainControllerTarget, "main.controller");
- assertFlagDataHasValue(archive, MY_TEST_FLAG, prodUsWestCfgTarget, "main.prod.us-west-1");
- assertFlagDataHasValue(archive, MY_TEST_FLAG, prodUsEast3CfgTarget, "main.prod");
- assertFlagDataHasValue(archive, MY_TEST_FLAG, devUsEast1CfgTarget, "main");
- }
-
- private static void assertFlagDataHasValue(SystemFlagsDataArchive archive, FlagId flagId, FlagsTarget target, String value) {
- List<FlagData> data = getData(archive, flagId, target);
- assertEquals(1, data.size());
- FlagData flagData = data.get(0);
- RawFlag rawFlag = flagData.resolve(new FetchVector()).get();
- assertEquals(String.format("\"%s\"", value), rawFlag.asJson());
- }
-
- private static void assertNoFlagData(SystemFlagsDataArchive archive, FlagId flagId, FlagsTarget target) {
- List<FlagData> data = getData(archive, flagId, target);
- assertTrue(data.isEmpty());
- }
-
- private static List<FlagData> getData(SystemFlagsDataArchive archive, FlagId flagId, FlagsTarget target) {
- return archive.flagData(target).stream()
- .filter(d -> d.id().equals(flagId))
- .toList();
- }
-
- private static class SimpleZone implements ZoneApi {
- final ZoneId zoneId;
- SimpleZone(String zoneId) { this.zoneId = ZoneId.from(zoneId); }
-
- @Override public SystemName getSystemName() { return SystemName.main; }
- @Override public ZoneId getId() { return zoneId; }
- @Override public CloudName getCloudName() { return CloudName.YAHOO; }
- @Override public String getCloudNativeRegionName() { throw new UnsupportedOperationException(); }
- }
-
-}
diff --git a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccessTest.java b/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccessTest.java
deleted file mode 100644
index bcff84884a4..00000000000
--- a/controller-api/src/test/java/com/yahoo/vespa/hosted/controller/tenant/ArchiveAccessTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tenant;
-
-import com.yahoo.text.Text;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author bjorncs
- */
-class ArchiveAccessTest {
-
- @Test
- void validatesUserProvidedIamRole() {
- assertValidIamRole("arn:aws:iam::012345678912:user/foo");
- assertValidIamRole("arn:aws:iam::012345678912:role/foo");
-
- assertInvalidIamRole("arn:aws:iam::012345678912:foo/foo", "Invalid resource type - must be either a 'role' or 'user'");
- assertInvalidIamRole("arn:aws:iam::012345678912:foo", "Missing resource type - must be 'role' or 'user'");
- assertInvalidIamRole("arn:aws:iam::012345678912:role", "Missing resource type - must be 'role' or 'user'");
- assertInvalidIamRole("arn:aws:iam::012345678912:", "Malformed ARN - no resource specified");
- assertInvalidIamRole("arn:aws:iam::01234567891:user/foo", "Account id must be a 12-digit number");
- assertInvalidIamRole("arn:gcp:iam::012345678912:user/foo", "Partition must be 'aws'");
- assertInvalidIamRole("uri:aws:iam::012345678912:user/foo", "Malformed ARN - doesn't start with 'arn:'");
- assertInvalidIamRole("arn:aws:s3:::mybucket", "Service must be 'iam'");
- assertInvalidIamRole("", "Malformed ARN - doesn't start with 'arn:'");
- assertInvalidIamRole("foo", "Malformed ARN - doesn't start with 'arn:'");
- }
-
- private static void assertValidIamRole(String role) { assertDoesNotThrow(() -> archiveAccess(role)); }
-
- private static void assertInvalidIamRole(String role, String expectedMessage) {
- var t = assertThrows(IllegalArgumentException.class, () -> archiveAccess(role));
- var expectedPrefix = Text.format("Invalid archive access IAM role '%s': ", role);
- System.out.println(t.getMessage());
- assertTrue(t.getMessage().startsWith(expectedPrefix), role);
- assertEquals(expectedMessage, t.getMessage().substring(expectedPrefix.length()));
- }
-
- private static ArchiveAccess archiveAccess(String iamRole) { return new ArchiveAccess().withAWSRole(iamRole); }
-
-}
diff --git a/controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/cd.controller.json b/controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/cd.controller.json
deleted file mode 100644
index ce3cdd43889..00000000000
--- a/controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/cd.controller.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "cd.controller"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/default.json b/controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/default.json
deleted file mode 100644
index 5924eb860c0..00000000000
--- a/controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/default.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/main.prod.us-west-1.json b/controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/main.prod.us-west-1.json
deleted file mode 100644
index 45989773df8..00000000000
--- a/controller-api/src/test/resources/system-flags-for-multiple-systems/flags/my-test-flag/main.prod.us-west-1.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "main.prod.us-west-1"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags-multi-level-with-duplicated-flagdata/flags/group-1/my-test-flag/default.json b/controller-api/src/test/resources/system-flags-multi-level-with-duplicated-flagdata/flags/group-1/my-test-flag/default.json
deleted file mode 100644
index 5924eb860c0..00000000000
--- a/controller-api/src/test/resources/system-flags-multi-level-with-duplicated-flagdata/flags/group-1/my-test-flag/default.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags-multi-level-with-duplicated-flagdata/flags/group-2/my-test-flag/default.json b/controller-api/src/test/resources/system-flags-multi-level-with-duplicated-flagdata/flags/group-2/my-test-flag/default.json
deleted file mode 100644
index 5924eb860c0..00000000000
--- a/controller-api/src/test/resources/system-flags-multi-level-with-duplicated-flagdata/flags/group-2/my-test-flag/default.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags-multi-level/flags/group-1/my-test-flag/default.json b/controller-api/src/test/resources/system-flags-multi-level/flags/group-1/my-test-flag/default.json
deleted file mode 100644
index 5924eb860c0..00000000000
--- a/controller-api/src/test/resources/system-flags-multi-level/flags/group-1/my-test-flag/default.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags-multi-level/flags/group-2/my-other-test-flag/default.json b/controller-api/src/test/resources/system-flags-multi-level/flags/group-2/my-other-test-flag/default.json
deleted file mode 100644
index e30485b755c..00000000000
--- a/controller-api/src/test/resources/system-flags-multi-level/flags/group-2/my-other-test-flag/default.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-other-test-flag",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags-with-invalid-file-name/flags/my-test-flag/file-name-without-dot-json b/controller-api/src/test/resources/system-flags-with-invalid-file-name/flags/my-test-flag/file-name-without-dot-json
deleted file mode 100644
index 5924eb860c0..00000000000
--- a/controller-api/src/test/resources/system-flags-with-invalid-file-name/flags/my-test-flag/file-name-without-dot-json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags-with-null-value/flags/my-test-flag/main.prod.us-east-3.json b/controller-api/src/test/resources/system-flags-with-null-value/flags/my-test-flag/main.prod.us-east-3.json
deleted file mode 100644
index c4dca9aa2e1..00000000000
--- a/controller-api/src/test/resources/system-flags-with-null-value/flags/my-test-flag/main.prod.us-east-3.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "instance",
- "values": ["a:b:c"]
- }
- ]
- },
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "hostname",
- "values": ["foo.com"]
- }
- ],
- "value" : true
- }
- ]
-}
diff --git a/controller-api/src/test/resources/system-flags-with-null-value/flags/my-test-flag/main.prod.us-west-1.json b/controller-api/src/test/resources/system-flags-with-null-value/flags/my-test-flag/main.prod.us-west-1.json
deleted file mode 100644
index 283b09d5c0b..00000000000
--- a/controller-api/src/test/resources/system-flags-with-null-value/flags/my-test-flag/main.prod.us-west-1.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "instance",
- "values": ["a:b:c"]
- }
- ],
- "value" : null
- },
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "hostname",
- "values": ["foo.com"]
- }
- ],
- "value" : true
- }
- ]
-}
diff --git a/controller-api/src/test/resources/system-flags-with-unknown-field-name/flags/my-test-flag/main.prod.us-west-1.json b/controller-api/src/test/resources/system-flags-with-unknown-field-name/flags/my-test-flag/main.prod.us-west-1.json
deleted file mode 100644
index c41083fc7ab..00000000000
--- a/controller-api/src/test/resources/system-flags-with-unknown-field-name/flags/my-test-flag/main.prod.us-west-1.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "condition": [
- {
- "type": "whitelist",
- "dimension": "hostname",
- "values": ["foo.com"]
- }
- ],
- "value" : "default"
- }
- ]
-}
diff --git a/controller-api/src/test/resources/system-flags-with-unknown-file-name/flags/my-test-flag/main.prod.unknown-region.json b/controller-api/src/test/resources/system-flags-with-unknown-file-name/flags/my-test-flag/main.prod.unknown-region.json
deleted file mode 100644
index 5924eb860c0..00000000000
--- a/controller-api/src/test/resources/system-flags-with-unknown-file-name/flags/my-test-flag/main.prod.unknown-region.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/default.json b/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/default.json
deleted file mode 100644
index 54aba0d9923..00000000000
--- a/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/default.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "flag-with-empty-data",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.controller.json b/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.controller.json
deleted file mode 100644
index c9b46d68ace..00000000000
--- a/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.controller.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "id" : "flag-with-empty-data",
- "comment": "empty data using only id field"
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.json b/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.json
deleted file mode 100644
index 29160dc0081..00000000000
--- a/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "comment": "comment a",
- "id" : "flag-with-empty-data",
- "rules" : [
- {
- "comment": "comment b",
- "value" : "main"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.prod.json b/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.prod.json
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.prod.json
+++ /dev/null
diff --git a/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.prod.us-west-1.json b/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.prod.us-west-1.json
deleted file mode 100644
index fc2690c9c04..00000000000
--- a/controller-api/src/test/resources/system-flags/flags/flag-with-empty-data/main.prod.us-west-1.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "flag-with-empty-data",
- "rules" : [
- {
- "value" : "main.prod.us-west-1"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags/flags/my-test-flag/default.json b/controller-api/src/test/resources/system-flags/flags/my-test-flag/default.json
deleted file mode 100644
index 5924eb860c0..00000000000
--- a/controller-api/src/test/resources/system-flags/flags/my-test-flag/default.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.controller.json b/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.controller.json
deleted file mode 100644
index 2860c833533..00000000000
--- a/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.controller.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "main.controller"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.json b/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.json
deleted file mode 100644
index d94390cd2a4..00000000000
--- a/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "main"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.prod.json b/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.prod.json
deleted file mode 100644
index 28d2f068160..00000000000
--- a/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.prod.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "main.prod"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.prod.us-west-1.json b/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.prod.us-west-1.json
deleted file mode 100644
index 45989773df8..00000000000
--- a/controller-api/src/test/resources/system-flags/flags/my-test-flag/main.prod.us-west-1.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-test-flag",
- "rules" : [
- {
- "value" : "main.prod.us-west-1"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/OWNERS b/controller-server/OWNERS
deleted file mode 100644
index c7b017cd739..00000000000
--- a/controller-server/OWNERS
+++ /dev/null
@@ -1,2 +0,0 @@
-mpolden
-jonmv
diff --git a/controller-server/README b/controller-server/README
deleted file mode 100644
index 476a95019b2..00000000000
--- a/controller-server/README
+++ /dev/null
@@ -1 +0,0 @@
-controller-server implements the control plane for a Vespa cloud system.
diff --git a/controller-server/pom.xml b/controller-server/pom.xml
deleted file mode 100644
index a9db2cace85..00000000000
--- a/controller-server/pom.xml
+++ /dev/null
@@ -1,284 +0,0 @@
-<?xml version="1.0"?>
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
- http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>parent</artifactId>
- <version>8-SNAPSHOT</version>
- <relativePath>../parent/pom.xml</relativePath>
- </parent>
- <artifactId>controller-server</artifactId>
- <packaging>container-plugin</packaging>
- <version>8-SNAPSHOT</version>
-
- <dependencies>
-
- <!-- provided -->
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>container-apache-http-client-bundle</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>controller-api</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>config-application-package</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>container-dev</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>zkfacade</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>config-provisioning</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>application-model</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>vespa-athenz</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>jdisc-security-filters</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.google.inject</groupId>
- <artifactId>guice</artifactId>
-
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.fasterxml.jackson.core</groupId>
- <artifactId>jackson-databind</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>flags</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>configserver-flags</artifactId>
- <version>${project.version}</version>
- <scope>provided</scope>
- </dependency>
-
- <!-- compile -->
-
- <dependency>
- <groupId>org.apache.velocity</groupId>
- <artifactId>velocity-engine-core</artifactId>
- <exclusions>
- <exclusion>
- <!-- Must use the one provided by Jdisc to prevent two instances of slf4j classes. -->
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
- <dependency>
- <groupId>org.apache.velocity.tools</groupId>
- <artifactId>velocity-tools-generic</artifactId>
- <exclusions>
- <exclusion>
- <!-- Must use the one provided by Jdisc to prevent two instances of slf4j classes. -->
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </exclusion>
- <exclusion>
- <groupId>commons-logging</groupId>
- <artifactId>commons-logging</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-csv</artifactId>
- </dependency>
-
- <dependency>
- <groupId>commons-fileupload</groupId>
- <artifactId>commons-fileupload</artifactId>
- <version>1.5</version>
- </dependency>
-
- <dependency>
- <groupId>com.auth0</groupId>
- <artifactId>java-jwt</artifactId>
- <exclusions>
- <exclusion>
- <groupId>com.fasterxml.jackson.core</groupId>
- <artifactId>jackson-databind</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>config-model-api</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>hosted-api</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>http-utils</artifactId>
- <version>${project.version}</version>
- </dependency>
-
- <dependency>
- <!-- required by java-jwt -->
- <groupId>commons-codec</groupId>
- <artifactId>commons-codec</artifactId>
- </dependency>
-
- <!-- test -->
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>application</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>testutil</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- <exclusions>
- <exclusion>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- </exclusion>
- <exclusion>
- <groupId>org.hamcrest</groupId>
- <artifactId>*</artifactId>
- </exclusion>
- </exclusions>
- </dependency>
-
- <dependency>
- <groupId>org.wiremock</groupId>
- <artifactId>wiremock-standalone</artifactId>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>org.assertj</groupId>
- <artifactId>assertj-core</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter-api</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.junit.jupiter</groupId>
- <artifactId>junit-jupiter-engine</artifactId>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-core</artifactId>
- <scope>test</scope>
- </dependency>
-
- <dependency>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>container-test</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency>
-
- </dependencies>
-
- <build>
- <plugins>
- <plugin>
- <groupId>com.yahoo.vespa</groupId>
- <artifactId>bundle-plugin</artifactId>
- <configuration>
- <attachBundleArtifact>true</attachBundleArtifact>
- <bundleClassifierName>deploy</bundleClassifierName>
- <useCommonAssemblyIds>false</useCommonAssemblyIds>
- <WebInfUrl>/WEB-INF/web.xml</WebInfUrl>
- </configuration>
- <extensions>true</extensions>
- </plugin>
-
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <configuration>
- <compilerArgs>
- <arg>-Xlint:all</arg>
- <arg>-Xlint:-serial</arg>
- <arg>-Xlint:-try</arg>
- <arg>-Xlint:-processing</arg>
- </compilerArgs>
- </configuration>
- </plugin>
- </plugins>
- </build>
-</project>
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
deleted file mode 100644
index 0e6f29c760d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Application.java
+++ /dev/null
@@ -1,254 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.AccountId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-import com.yahoo.vespa.hosted.controller.application.ApplicationActivity;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
-import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.security.PublicKey;
-import java.time.Instant;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.OptionalLong;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * An application. Belongs to a {@link Tenant}, and may have multiple {@link Instance}s.
- *
- * This is immutable.
- *
- * @author jonmv
- */
-public class Application {
-
- private final TenantAndApplicationId id;
- private final Instant createdAt;
- private final DeploymentSpec deploymentSpec;
- private final ValidationOverrides validationOverrides;
- private final RevisionHistory revisions;
- private final OptionalLong projectId;
- private final Optional<IssueId> deploymentIssueId;
- private final Optional<IssueId> ownershipIssueId;
- private final Optional<User> userOwner;
- private final Optional<AccountId> issueOwner;
- private final OptionalInt majorVersion;
- private final ApplicationMetrics metrics;
- private final Set<PublicKey> deployKeys;
- private final Map<InstanceName, Instance> instances;
-
- /** Creates an empty application. */
- public Application(TenantAndApplicationId id, Instant now) {
- this(id, now, DeploymentSpec.empty, ValidationOverrides.empty, Optional.empty(), Optional.empty(),
- Optional.empty(), Optional.empty(), OptionalInt.empty(), new ApplicationMetrics(0, 0),
- Set.of(), OptionalLong.empty(), RevisionHistory.empty(), List.of());
- }
-
- // Do not use directly - edit through LockedApplication.
- public Application(TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec, ValidationOverrides validationOverrides,
- Optional<IssueId> deploymentIssueId, Optional<IssueId> ownershipIssueId, Optional<User> userOwner, Optional<AccountId> issueOwner,
- OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys, OptionalLong projectId,
- RevisionHistory revisions, Collection<Instance> instances) {
- this.id = Objects.requireNonNull(id, "id cannot be null");
- this.createdAt = Objects.requireNonNull(createdAt, "instant of creation cannot be null");
- this.deploymentSpec = Objects.requireNonNull(deploymentSpec, "deploymentSpec cannot be null");
- this.validationOverrides = Objects.requireNonNull(validationOverrides, "validationOverrides cannot be null");
- this.deploymentIssueId = Objects.requireNonNull(deploymentIssueId, "deploymentIssueId cannot be null");
- this.ownershipIssueId = Objects.requireNonNull(ownershipIssueId, "ownershipIssueId cannot be null");
- this.userOwner = Objects.requireNonNull(userOwner, "owner cannot be null");
- this.issueOwner = Objects.requireNonNull(issueOwner, "issueOwner cannot be null");
- this.majorVersion = Objects.requireNonNull(majorVersion, "majorVersion cannot be null");
- this.metrics = Objects.requireNonNull(metrics, "metrics cannot be null");
- this.deployKeys = Objects.requireNonNull(deployKeys, "deployKeys cannot be null");
- this.projectId = Objects.requireNonNull(projectId, "projectId cannot be null");
- this.revisions = revisions;
- this.instances = instances.stream().collect(
- Collectors.collectingAndThen(Collectors.toMap(Instance::name,
- Function.identity(),
- (i1, i2) -> {
- throw new IllegalArgumentException("Duplicate instance " + i1.id());
- },
- TreeMap::new),
- Collections::unmodifiableMap)
- );
- }
-
- public TenantAndApplicationId id() { return id; }
-
- public Instant createdAt() { return createdAt; }
-
- /**
- * Returns the last deployed deployment spec of this application,
- * or the empty deployment spec if it has never been deployed
- */
- public DeploymentSpec deploymentSpec() { return deploymentSpec; }
-
- /** Returns the project id of this application, if it has any. */
- public OptionalLong projectId() { return projectId; }
-
- /** Returns the known revisions for this application. */
- public RevisionHistory revisions() { return revisions; }
-
- /**
- * Returns the last deployed validation overrides of this application,
- * or the empty validation overrides if it has never been deployed
- * (or was deployed with an empty/missing validation overrides)
- */
- public ValidationOverrides validationOverrides() { return validationOverrides; }
-
- /** Returns the instances of this application */
- public Map<InstanceName, Instance> instances() { return instances; }
-
- /** Returns the instances of this application which are defined in its deployment spec. */
- public Map<InstanceName, Instance> productionInstances() {
- return deploymentSpec.instanceNames().stream()
- .collect(Collectors.toUnmodifiableMap(Function.identity(), instances::get));
- }
-
- /** Returns the instance with the given name, if it exists. */
- public Optional<Instance> get(InstanceName instance) { return Optional.ofNullable(instances.get(instance)); }
-
- /** Returns the instance with the given name, or throws. */
- public Instance require(InstanceName instance) {
- return get(instance).orElseThrow(() -> new IllegalArgumentException("Unknown instance '" + instance + "' in '" + id + "'"));
- }
-
- /** Returns ID of any open deployment issue filed for this */
- public Optional<IssueId> deploymentIssueId() {
- return deploymentIssueId;
- }
-
- /** Returns ID of the last ownership issue filed for this */
- public Optional<IssueId> ownershipIssueId() {
- return ownershipIssueId;
- }
-
- public Optional<User> userOwner() {
- return userOwner;
- }
-
- public Optional<AccountId> issueOwner() {
- return issueOwner;
- }
-
- /**
- * Overrides the system major version for this application. This override takes effect if the deployment
- * spec does not specify a major version.
- */
- public OptionalInt majorVersion() { return majorVersion; }
-
- /** Returns metrics for this */
- public ApplicationMetrics metrics() {
- return metrics;
- }
-
- /** Returns activity for this */
- public ApplicationActivity activity() {
- return ApplicationActivity.from(instances.values().stream()
- .flatMap(instance -> instance.deployments().values().stream())
- .toList());
- }
-
- public Map<InstanceName, List<Deployment>> productionDeployments() {
- return instances.values().stream()
- .collect(Collectors.toUnmodifiableMap(Instance::name,
- instance -> List.copyOf(instance.productionDeployments().values())));
- }
- /**
- * Returns the oldest platform version this has deployed in a permanent zone (not test or staging).
- *
- * This is unfortunately quite similar to {@link ApplicationController#oldestInstalledPlatform(Application)},
- * but this checks only what the controller has deployed to the production zones, while that checks the node repository
- * to see what's actually installed on each node. Thus, this is the right choice for, e.g., target Vespa versions for
- * new deployments, while that is the right choice for version to compile against.
- */
- public Optional<Version> oldestDeployedPlatform() {
- return productionDeployments().values().stream().flatMap(List::stream)
- .map(Deployment::version)
- .min(Comparator.naturalOrder());
- }
-
- /** Returns the oldest application version this has deployed in a permanent zone (not test or staging) */
- public Optional<RevisionId> oldestDeployedRevision() {
- return productionRevisions().min(Comparator.naturalOrder());
- }
-
- /** Returns the latest application version this has deployed in a permanent zone (not test or staging) */
- public Optional<RevisionId> latestDeployedRevision() {
- return productionRevisions().max(Comparator.naturalOrder());
- }
-
- private Stream<RevisionId> productionRevisions() {
- return productionDeployments().values().stream().flatMap(List::stream)
- .map(Deployment::revision)
- .filter(RevisionId::isProduction);
- }
-
- /** Returns the total quota usage for this application, excluding temporary deployments */
- public QuotaUsage quotaUsage() {
- return instances().values().stream()
- .map(Instance::quotaUsage)
- .reduce(QuotaUsage::add)
- .orElse(QuotaUsage.none);
- }
-
- /** Returns the total quota usage for manual deployments for this application */
- public QuotaUsage manualQuotaUsage() {
- return instances().values().stream()
- .map(Instance::manualQuotaUsage)
- .reduce(QuotaUsage::add)
- .orElse(QuotaUsage.none);
- }
-
- /** Returns the total quota usage for this application, excluding one specific deployment (and temporary deployments) */
- public QuotaUsage quotaUsage(ApplicationId application, ZoneId zone) {
- return instances().values().stream()
- .map(instance -> instance.quotaUsageExcluding(application, zone))
- .reduce(QuotaUsage::add)
- .orElse(QuotaUsage.none);
- }
-
- /** Returns the set of deploy keys for this application. */
- public Set<PublicKey> deployKeys() { return deployKeys; }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (! (o instanceof Application other)) return false;
- return id.equals(other.id);
- }
-
- @Override
- public int hashCode() {
- return id.hashCode();
- }
-
- @Override
- public String toString() {
- return "application '" + id + "'";
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
deleted file mode 100644
index d7a3d4fb9e5..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/ApplicationController.java
+++ /dev/null
@@ -1,1111 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.component.Version;
-import com.yahoo.component.VersionCompatibility;
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.DockerImage;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.Tags;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.text.Text;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.AthenzService;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.ListFlag;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.flags.StringFlag;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeploymentData;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeploymentEndpoints;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.InstanceId;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.DeploymentResult;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.DeploymentResult.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationStore;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
-import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics.Warning;
-import com.yahoo.vespa.hosted.controller.application.DeploymentQuotaCalculator;
-import com.yahoo.vespa.hosted.controller.application.EndpointList;
-import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageStream;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageValidator;
-import com.yahoo.vespa.hosted.controller.application.pkg.BasicServicesXml;
-import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
-import com.yahoo.vespa.hosted.controller.certificate.EndpointCertificates;
-import com.yahoo.vespa.hosted.controller.concurrent.Once;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
-import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.notification.Notification;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.routing.PreparedEndpoints;
-import com.yahoo.vespa.hosted.controller.security.AccessControl;
-import com.yahoo.vespa.hosted.controller.security.Credentials;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccessGrant;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-import com.yahoo.yolean.Exceptions;
-
-import java.io.ByteArrayInputStream;
-import java.security.Principal;
-import java.security.cert.X509Certificate;
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-import java.util.function.UnaryOperator;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.vespa.flags.FetchVector.Dimension.INSTANCE_ID;
-import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.active;
-import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.reserved;
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.broken;
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.high;
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.low;
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.normal;
-import static java.util.Comparator.naturalOrder;
-import static java.util.stream.Collectors.collectingAndThen;
-import static java.util.stream.Collectors.counting;
-import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toMap;
-import static java.util.stream.Collectors.toSet;
-
-/**
- * A singleton owned by {@link Controller} which contains the methods and state for controlling applications.
- *
- * @author bratseth
- */
-public class ApplicationController {
-
- private static final Logger log = Logger.getLogger(ApplicationController.class.getName());
-
- /** The controller owning this */
- private final Controller controller;
-
- /** For persistence */
- private final CuratorDb curator;
-
- private final ArtifactRepository artifactRepository;
- private final ApplicationStore applicationStore;
- private final AccessControl accessControl;
- private final ConfigServer configServer;
- private final Clock clock;
- private final DeploymentTrigger deploymentTrigger;
- private final ApplicationPackageValidator applicationPackageValidator;
- private final EndpointCertificates endpointCertificates;
- private final StringFlag dockerImageRepoFlag;
- private final ListFlag<String> incompatibleVersions;
- private final BillingController billingController;
- private final ListFlag<String> cloudAccountsFlag;
-
- private final Map<DeploymentId, com.yahoo.vespa.hosted.controller.api.integration.configserver.Application> deploymentInfo = new ConcurrentHashMap<>();
-
- ApplicationController(Controller controller, CuratorDb curator, AccessControl accessControl, Clock clock,
- FlagSource flagSource, BillingController billingController) {
- this.controller = Objects.requireNonNull(controller);
- this.curator = Objects.requireNonNull(curator);
- this.accessControl = Objects.requireNonNull(accessControl);
- this.configServer = controller.serviceRegistry().configServer();
- this.clock = Objects.requireNonNull(clock);
- this.billingController = Objects.requireNonNull(billingController);
-
- artifactRepository = controller.serviceRegistry().artifactRepository();
- applicationStore = controller.serviceRegistry().applicationStore();
- dockerImageRepoFlag = PermanentFlags.DOCKER_IMAGE_REPO.bindTo(flagSource);
- incompatibleVersions = PermanentFlags.INCOMPATIBLE_VERSIONS.bindTo(flagSource);
- cloudAccountsFlag = PermanentFlags.CLOUD_ACCOUNTS.bindTo(flagSource);
- deploymentTrigger = new DeploymentTrigger(controller, clock);
- applicationPackageValidator = new ApplicationPackageValidator(controller);
- endpointCertificates = new EndpointCertificates(controller,
- controller.serviceRegistry().endpointCertificateProvider(),
- controller.serviceRegistry().endpointCertificateValidator());
-
- // Update serialization format of all applications
- Once.after(Duration.ofMinutes(1), () -> {
- Instant start = clock.instant();
- int count = 0;
- for (TenantAndApplicationId id : curator.readApplicationIds()) {
- lockApplicationIfPresent(id, application -> {
- for (var declaredInstance : application.get().deploymentSpec().instances())
- if ( ! application.get().instances().containsKey(declaredInstance.name()))
- application = withNewInstance(application, id.instance(declaredInstance.name()));
- store(application);
- });
- count++;
- }
- log.log(Level.INFO, Text.format("Wrote %d applications in %s", count,
- Duration.between(start, clock.instant())));
- });
- }
-
- /** Validate the given application package */
- public void validatePackage(ApplicationPackage applicationPackage, Application application) {
- applicationPackageValidator.validate(application, applicationPackage, clock.instant());
- }
-
- public Set<CloudAccount> accountsOf(TenantName tenant) {
- return cloudAccountsFlag.with(FetchVector.Dimension.TENANT_ID, tenant.value())
- .value().stream()
- .map(CloudAccount::from)
- .collect(Collectors.toSet());
- }
-
- /** Returns the application with the given id, or null if it is not present */
- public Optional<Application> getApplication(TenantAndApplicationId id) {
- return curator.readApplication(id);
- }
-
- /** Returns the instance with the given id, or null if it is not present */
- public Optional<Instance> getInstance(ApplicationId id) {
- return getApplication(TenantAndApplicationId.from(id)).flatMap(application -> application.get(id.instance()));
- }
-
- /**
- * Returns in-memory info for the given deployment pulled from the node repo.
- * Info on any existing deployment can be missing if it has not yet been fetched since this instance was started.
- * This is kept up to date by DeploymentInfoMaintainer.
- * Accessing this is thread safe.
- */
- // TODO: Replace the wire level Application by a DeploymentInfo class in the model
- public Map<DeploymentId, com.yahoo.vespa.hosted.controller.api.integration.configserver.Application> deploymentInfo() { return deploymentInfo; }
-
- /**
- * Triggers reindexing for the given document types in the given clusters, for the given application.
- * <p>
- * If no clusters are given, reindexing is triggered for the entire application; otherwise
- * if no documents types are given, reindexing is triggered for all given clusters; otherwise
- * reindexing is triggered for the cartesian product of the given clusters and document types.
- */
- public void reindex(ApplicationId id, ZoneId zoneId, List<String> clusterNames, List<String> documentTypes, boolean indexedOnly, Double speed, String cause) {
- configServer.reindex(new DeploymentId(id, zoneId), clusterNames, documentTypes, indexedOnly, speed, cause);
- }
-
- /** Returns the reindexing status for the given application in the given zone. */
- public ApplicationReindexing applicationReindexing(ApplicationId id, ZoneId zoneId) {
- return configServer.getReindexing(new DeploymentId(id, zoneId));
- }
-
- /** Enables reindexing for the given application in the given zone. */
- public void enableReindexing(ApplicationId id, ZoneId zoneId) {
- configServer.enableReindexing(new DeploymentId(id, zoneId));
- }
-
- /** Disables reindexing for the given application in the given zone. */
- public void disableReindexing(ApplicationId id, ZoneId zoneId) {
- configServer.disableReindexing(new DeploymentId(id, zoneId));
- }
-
- /**
- * Returns the application with the given id
- *
- * @throws IllegalArgumentException if it does not exist
- */
- public Application requireApplication(TenantAndApplicationId id) {
- return getApplication(id).orElseThrow(() -> new IllegalArgumentException(id + " not found"));
- }
-
- /**
- * Returns the instance with the given id
- *
- * @throws IllegalArgumentException if it does not exist
- */
- // TODO jonvm: remove or inline
- public Instance requireInstance(ApplicationId id) {
- return getInstance(id).orElseThrow(() -> new IllegalArgumentException(id + " not found"));
- }
-
- /** Returns a snapshot of all applications */
- public List<Application> asList() {
- return curator.readApplications(false);
- }
-
- /**
- * Returns a snapshot of all readable applications. Unlike {@link ApplicationController#asList()} this ignores
- * applications that cannot currently be read (e.g. due to serialization issues) and may return an incomplete
- * snapshot.
- *
- * This should only be used in cases where acting on a subset of applications is better than none.
- */
- public List<Application> readable() {
- return curator.readApplications(true);
- }
-
- /** Returns the ID of all known applications. */
- public List<TenantAndApplicationId> idList() {
- return curator.readApplicationIds();
- }
-
- /** Returns a snapshot of all applications of a tenant */
- public List<Application> asList(TenantName tenant) {
- return curator.readApplications(tenant);
- }
-
- public ArtifactRepository artifacts() { return artifactRepository; }
-
- public ApplicationStore applicationStore() { return applicationStore; }
-
- /** Returns all currently reachable content clusters among the given deployments. */
- public Map<ZoneId, List<String>> reachableContentClustersByZone(Collection<DeploymentId> ids) {
- Map<ZoneId, List<String>> clusters = new TreeMap<>(Comparator.comparing(ZoneId::value));
- for (DeploymentId id : ids)
- if (isHealthy(id))
- clusters.put(id.zoneId(), List.copyOf(configServer.getContentClusters(id)));
-
- return Collections.unmodifiableMap(clusters);
- }
-
- /** Reads the oldest installed platform for the given application and zone from job history, or a node repo. */
- private Optional<Version> oldestInstalledPlatform(JobStatus job) {
- Version oldest = null;
- for (Run run : job.runs().descendingMap().values()) {
- Version version = run.versions().targetPlatform();
- if (oldest == null || version.isBefore(oldest))
- oldest = version;
-
- if (run.hasSucceeded())
- return Optional.of(oldest);
- }
- // If no successful run was found, ask the node repository in the relevant zone.
- return oldestInstalledPlatform(job.id());
- }
-
- /** Reads the oldest installed platform for the given application and zone from the node repo of that zone. */
- private Optional<Version> oldestInstalledPlatform(JobId job) {
- return configServer.nodeRepository().list(job.type().zone(),
- NodeFilter.all()
- .applications(job.application())
- .states(active, reserved))
- .stream()
- .map(Node::currentVersion)
- .filter(version -> ! version.isEmpty())
- .min(naturalOrder());
- }
-
- /** Returns the oldest Vespa version installed on any active or reserved production node for the given application. */
- public Optional<Version> oldestInstalledPlatform(Application application) {
- return controller.jobController().deploymentStatus(application).jobs()
- .production()
- .not().test()
- .asList().stream()
- .map(this::oldestInstalledPlatform)
- .flatMap(Optional::stream)
- .min(naturalOrder());
- }
-
- /**
- * Returns the preferred Vespa version to compile against, for
- * <p>
- * The returned version is not newer than the oldest deployed platform for the application, unless
- * the target major differs from the oldest deployed platform, in which case it is not newer than
- * the oldest available platform version on that major instead.
- * <p>
- * The returned version is compatible with a platform version available in the system.
- * <p>
- * A candidate is sought first among versions with non-broken confidence, then among those with forgotten confidence.
- * <p>
- * The returned version is the latest in the relevant candidate set.
- * <p>
- * If no such version exists, an {@link IllegalArgumentException} is thrown.
- */
- public Version compileVersion(TenantAndApplicationId id, OptionalInt wantedMajor) {
-
- // Read version status, and pick out target platforms we could run the compiled package on.
- Optional<Application> application = getApplication(id);
- Optional<Version> oldestInstalledPlatform = application.flatMap(this::oldestInstalledPlatform);
- VersionStatus versionStatus = controller.readVersionStatus();
- UpgradePolicy policy = application.flatMap(app -> app.deploymentSpec().instances().stream()
- .map(DeploymentInstanceSpec::upgradePolicy)
- .max(naturalOrder()))
- .orElse(UpgradePolicy.defaultPolicy);
- Confidence targetConfidence = switch (policy) {
- case canary -> broken;
- case defaultPolicy -> normal;
- case conservative -> high;
- };
-
- // Target platforms are all versions not older than the oldest installed platform, unless forcing a major version change.
- // Only platforms not older than the system version, and with appropriate confidence, are considered targets.
- Predicate<Version> isTargetPlatform = wantedMajor.isEmpty() && oldestInstalledPlatform.isEmpty()
- ? __ -> true // No preferences for version: any platform version is ok.
- : wantedMajor.isEmpty() || (oldestInstalledPlatform.isPresent() && wantedMajor.getAsInt() == oldestInstalledPlatform.get().getMajor())
- ? version -> ! version.isBefore(oldestInstalledPlatform.get()) // Major empty, or on same as oldest: ensure not a platform downgrade.
- : version -> wantedMajor.getAsInt() == version.getMajor(); // Major specified, and not on same as oldest (possibly empty): any on that major.
- Set<Version> platformVersions = versionStatus.deployableVersions().stream()
- .filter(version -> version.confidence().equalOrHigherThan(targetConfidence))
- .map(VespaVersion::versionNumber)
- .filter(isTargetPlatform)
- .collect(toSet());
- oldestInstalledPlatform.ifPresent(fallback -> {
- if (wantedMajor.isEmpty() || wantedMajor.getAsInt() == fallback.getMajor())
- platformVersions.add(fallback);
- });
-
- if (platformVersions.isEmpty())
- throw new IllegalArgumentException("this system has no available versions" +
- (wantedMajor.isPresent() ? " on specified major: " + wantedMajor.getAsInt() : ""));
-
- // The returned compile version must be compatible with at least one target platform.
- // If it is incompatible with any of the current platforms, the system will trigger a platform change.
- // The returned compile version should also be at least as old as both the oldest target platform version,
- // and the oldest current platform, unless the two are incompatible, in which case only the target matters,
- // or there are no installed platforms, in which case we prefer the newest target platform.
- VersionCompatibility compatibility = versionCompatibility(id.defaultInstance()); // Wrong id level >_<
- Version oldestTargetPlatform = platformVersions.stream().min(naturalOrder()).get();
- Version newestVersion = oldestInstalledPlatform.isEmpty()
- ? platformVersions.stream().max(naturalOrder()).get()
- : compatibility.accept(oldestInstalledPlatform.get(), oldestTargetPlatform)
- && oldestInstalledPlatform.get().isBefore(oldestTargetPlatform)
- ? oldestInstalledPlatform.get()
- : oldestTargetPlatform;
- Predicate<Version> systemCompatible = version -> ! version.isAfter(newestVersion)
- && platformVersions.stream().anyMatch(platform -> compatibility.accept(platform, version));
-
- // Find the newest, system-compatible version with non-broken confidence.
- Optional<Version> nonBroken = versionStatus.versions().stream()
- .filter(VespaVersion::isReleased)
- .filter(version -> version.confidence().equalOrHigherThan(low))
- .map(VespaVersion::versionNumber)
- .filter(systemCompatible)
- .max(naturalOrder());
-
- // Fall back to the newest, system-compatible version with unknown confidence. For public systems, this implies high confidence.
- Set<Version> knownVersions = versionStatus.versions().stream().map(VespaVersion::versionNumber).collect(toSet());
- Optional<Version> unknown = controller.mavenRepository().metadata().versions(clock.instant()).stream()
- .filter(version -> ! knownVersions.contains(version))
- .filter(systemCompatible)
- .max(naturalOrder());
-
- if (nonBroken.isPresent()) {
- if (controller.system().isPublic() && unknown.isPresent() && unknown.get().isAfter(nonBroken.get()))
- return unknown.get();
-
- return nonBroken.get();
- }
-
- if (unknown.isPresent())
- return unknown.get();
-
- throw new IllegalArgumentException("no suitable, released compile version exists" +
- (wantedMajor.isPresent() ? " for specified major: " + wantedMajor.getAsInt() : ""));
- }
-
- /**
- * Creates a new application for an existing tenant.
- *
- * @throws IllegalArgumentException if the application already exists
- */
- public Application createApplication(TenantAndApplicationId id, Credentials credentials) {
- try (Mutex lock = lock(id)) {
- if (getApplication(id).isPresent())
- throw new IllegalArgumentException("Could not create '" + id + "': Application already exists");
- if (getApplication(dashToUnderscore(id)).isPresent()) // VESPA-1945
- throw new IllegalArgumentException("Could not create '" + id + "': Application " + dashToUnderscore(id) + " already exists");
-
- com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId.validate(id.application().value());
-
- if (controller.tenants().get(id.tenant()).isEmpty())
- throw new IllegalArgumentException("Could not create '" + id + "': This tenant does not exist");
- accessControl.createApplication(id, credentials);
-
- LockedApplication locked = new LockedApplication(new Application(id, clock.instant()), lock);
- store(locked);
- log.info("Created " + locked);
- return locked.get();
- }
- }
-
- /**
- * Creates a new instance for an existing application.
- *
- * @throws IllegalArgumentException if the instance already exists, or has an invalid instance name.
- */
- public void createInstance(ApplicationId id) {
- lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- store(withNewInstance(application, id));
- });
- }
-
- /** Returns given application with a new instance */
- public LockedApplication withNewInstance(LockedApplication application, ApplicationId instance) {
- if (instance.instance().isTester())
- throw new IllegalArgumentException("'" + instance + "' is a tester application!");
- InstanceId.validate(instance.instance().value());
-
- if (getInstance(instance).isPresent())
- throw new IllegalArgumentException("Could not create '" + instance + "': Instance already exists");
- if (getInstance(dashToUnderscore(instance)).isPresent()) // VESPA-1945
- throw new IllegalArgumentException("Could not create '" + instance + "': Instance " + dashToUnderscore(instance) + " already exists");
-
- log.info("Created " + instance);
- return application.withNewInstance(instance.instance());
- }
-
- /** Deploys an application package for an existing application instance. */
- public DeploymentResult deploy(JobId job, boolean deploySourceVersions, Consumer<String> deployLogger, UnaryOperator<Optional<CloudAccount>> cloudAccountOverride) {
- if (job.application().instance().isTester())
- throw new IllegalArgumentException("'" + job.application() + "' is a tester application!");
-
- TenantAndApplicationId applicationId = TenantAndApplicationId.from(job.application());
- ZoneId zone = job.type().zone();
- DeploymentId deployment = new DeploymentId(job.application(), zone);
-
- try (Mutex deploymentLock = lockForDeployment(job.application(), zone)) {
- Run run = controller.jobController().last(job)
- .orElseThrow(() -> new IllegalStateException("No known run of '" + job + "'"));
-
- if (run.hasEnded())
- throw new IllegalStateException("No deployment expected for " + job + " now, as no job is running");
-
- Version platform = run.versions().sourcePlatform().filter(__ -> deploySourceVersions).orElse(run.versions().targetPlatform());
- RevisionId revision = run.versions().sourceRevision().filter(__ -> deploySourceVersions).orElse(run.versions().targetRevision());
- ApplicationPackageStream applicationPackage = new ApplicationPackageStream(() -> applicationStore.stream(deployment, revision));
- AtomicReference<RevisionId> lastRevision = new AtomicReference<>();
- // Prepare endpoints lazily
- Supplier<PreparedEndpoints> preparedEndpoints = () -> {
- try (Mutex lock = lock(applicationId)) {
- LockedApplication application = new LockedApplication(requireApplication(applicationId), lock);
- application.get().revisions().last().map(ApplicationVersion::id).ifPresent(lastRevision::set);
- return prepareEndpoints(deployment, job, application, applicationPackage, deployLogger, lock);
- }
- };
-
- // Carry out deployment without holding the application lock.
- DeploymentDataAndResult dataAndResult = deploy(job.application(), applicationPackage, zone, platform, preparedEndpoints,
- run.isDryRun(), run.testerCertificate(), cloudAccountOverride);
-
- // Record the quota usage for this application
- var quotaUsage = deploymentQuotaUsage(zone, job.application());
-
- // For direct deployments use the full deployment ID, but otherwise use just the tenant and application as
- // the source since it's the same application, so it should have the same warnings.
- // These notifications are only updated when the last submitted revision is deployed here.
- NotificationSource source = zone.environment().isManuallyDeployed()
- ? NotificationSource.from(deployment)
- : revision.equals(lastRevision.get()) ? NotificationSource.from(applicationId) : null;
- if (source != null) {
- List<String> warnings = Optional.ofNullable(dataAndResult.result().log())
- .map(logs -> logs.stream()
- .filter(LogEntry::concernsPackage)
- .filter(log -> log.level().intValue() >= Level.WARNING.intValue())
- .map(LogEntry::message)
- .sorted()
- .distinct()
- .toList())
- .orElseGet(List::of);
- if (warnings.isEmpty())
- controller.notificationsDb().removeNotification(source, Notification.Type.applicationPackage);
- else
- controller.notificationsDb().setApplicationPackageNotification(source, warnings);
- }
-
- lockApplicationOrThrow(applicationId, application ->
- store(application.with(job.application().instance(),
- i -> i.withNewDeployment(zone, revision, platform,
- clock.instant(), warningsFrom(dataAndResult.result().log()),
- quotaUsage, dataAndResult.data().cloudAccount().orElse(CloudAccount.empty),
- dataAndResult.data.dataPlaneTokens()))));
- return dataAndResult.result();
- }
- }
-
- private PreparedEndpoints prepareEndpoints(DeploymentId deployment, JobId job, LockedApplication application,
- ApplicationPackageStream applicationPackage,
- Consumer<String> deployLogger,
- Mutex applicationLock) {
- Instance instance = application.get().require(job.application().instance());
- Tags tags = applicationPackage.truncatedPackage().deploymentSpec().instance(instance.name())
- .map(DeploymentInstanceSpec::tags)
- .orElseGet(Tags::empty);
- EndpointCertificate certificate = endpointCertificates.get(deployment,
- applicationPackage.truncatedPackage().deploymentSpec(),
- applicationLock);
- deployLogger.accept("Using CA signed certificate version %s".formatted(certificate.version()));
- BasicServicesXml services = applicationPackage.truncatedPackage().services(deployment, tags);
- return controller.routing().of(deployment).prepare(services, certificate, application);
- }
-
- /** Stores the deployment spec and validation overrides from the application package, and runs cleanup. Returns new instances. */
- public List<InstanceName> storeWithUpdatedConfig(LockedApplication application, ApplicationPackage applicationPackage) {
- validatePackage(applicationPackage, application.get());
-
- application = application.with(applicationPackage.deploymentSpec());
- application = application.with(applicationPackage.validationOverrides());
-
- var existingInstances = application.get().instances();
- var declaredInstances = applicationPackage.deploymentSpec().instances();
- for (var declaredInstance : declaredInstances) {
- if ( ! existingInstances.containsKey(declaredInstance.name()))
- application = withNewInstance(application, application.get().id().instance(declaredInstance.name()));
- }
-
- // Delete zones not listed in DeploymentSpec, if allowed
- // We do this at deployment time for externally built applications, and at submission time
- // for internally built ones, to be able to return a validation failure message when necessary
- for (InstanceName name : existingInstances.keySet()) {
- application = withoutDeletedDeployments(application, name);
- }
-
- // Validate new deployment spec thoroughly before storing it.
- DeploymentStatus status = controller.jobController().deploymentStatus(application.get());
- Change dummyChange = Change.of(RevisionId.forProduction(Long.MAX_VALUE)); // Should always run everywhere.
- for (var jobs : status.jobsToRun(applicationPackage.deploymentSpec().instanceNames().stream()
- .collect(toMap(name -> name, __ -> dummyChange)))
- .entrySet()) {
- for (var job : jobs.getValue()) {
- decideCloudAccountOf(new DeploymentId(jobs.getKey().application(), job.type().zone()),
- applicationPackage.deploymentSpec());
- }
- }
-
- for (Notification notification : controller.notificationsDb().listNotifications(NotificationSource.from(application.get().id()), true)) {
- if ( notification.source().instance().isPresent()
- && ( ! declaredInstances.contains(notification.source().instance().get())
- || ! notification.source().zoneId().map(application.get().require(notification.source().instance().get()).deployments()::containsKey).orElse(false)))
- controller.notificationsDb().removeNotifications(notification.source());
- }
-
- store(application);
- return declaredInstances.stream()
- .map(DeploymentInstanceSpec::name)
- .filter(instance -> ! existingInstances.containsKey(instance))
- .toList();
- }
-
- /** Deploy a system application to given zone */
- public void deploy(SystemApplication application, ZoneId zone, Version version, boolean allowDowngrade) {
- if (application.hasApplicationPackage()) {
- deploySystemApplicationPackage(application, zone, version);
- } else {
- // Deploy by calling node repository directly
- configServer.nodeRepository().upgrade(zone, application.nodeType(), version, allowDowngrade);
- }
- }
-
- /** Deploy a system application to given zone */
- public DeploymentResult deploySystemApplicationPackage(SystemApplication application, ZoneId zone, Version version) {
- if (application.hasApplicationPackage()) {
- ApplicationPackageStream applicationPackage = new ApplicationPackageStream(
- () -> new ByteArrayInputStream(artifactRepository.getSystemApplicationPackage(application.id(), zone, version))
- );
- return deploy(application.id(), applicationPackage, zone, version, null, false, Optional.empty(), UnaryOperator.identity()).result();
- } else {
- throw new RuntimeException("This system application does not have an application package: " + application.id().toShortString());
- }
- }
-
- /** Deploys the given tester application to the given zone. */
- public DeploymentResult deployTester(TesterId tester, ApplicationPackageStream applicationPackage, ZoneId zone, Version platform, UnaryOperator<Optional<CloudAccount>> cloudAccountOverride) {
- return deploy(tester.id(), applicationPackage, zone, platform, null, false, Optional.empty(), cloudAccountOverride).result();
- }
-
- private record DeploymentDataAndResult(DeploymentData data, DeploymentResult result) {}
-
- private DeploymentDataAndResult deploy(ApplicationId application, ApplicationPackageStream applicationPackage,
- ZoneId zone, Version platform, Supplier<PreparedEndpoints> preparedEndpoints,
- boolean dryRun, Optional<X509Certificate> testerCertificate,
- UnaryOperator<Optional<CloudAccount>> cloudAccountOverride) {
- DeploymentId deployment = new DeploymentId(application, zone);
- // Routing and metadata may have changed, so we need to refresh state after deployment, even if deployment fails.
- interface CleanCloseable extends AutoCloseable { void close(); }
- AtomicReference<EndpointList> generatedEndpoints = new AtomicReference<>(EndpointList.EMPTY);
- try (CleanCloseable postDeployment = () -> updateRoutingAndMeta(deployment, applicationPackage, generatedEndpoints)) {
- Optional<DockerImage> dockerImageRepo = Optional.ofNullable(
- dockerImageRepoFlag
- .with(FetchVector.Dimension.ZONE_ID, zone.value())
- .with(INSTANCE_ID, application.serializedForm())
- .value())
- .filter(s -> !s.isBlank())
- .map(DockerImage::fromString);
-
- Optional<AthenzDomain> domain = controller.tenants().get(application.tenant())
- .filter(tenant-> tenant instanceof AthenzTenant)
- .map(tenant -> ((AthenzTenant)tenant).domain());
-
- Supplier<Quota> deploymentQuota = () -> DeploymentQuotaCalculator.calculate(billingController.getQuota(application.tenant()),
- asList(application.tenant()), application, zone, applicationPackage.truncatedPackage().deploymentSpec());
-
- List<TenantSecretStore> tenantSecretStores = controller.tenants()
- .get(application.tenant())
- .filter(tenant-> tenant instanceof CloudTenant)
- .map(tenant -> ((CloudTenant) tenant).tenantSecretStores())
- .orElse(List.of());
- List<X509Certificate> operatorCertificates = controller.supportAccess().activeGrantsFor(deployment).stream()
- .map(SupportAccessGrant::certificate)
- .toList();
- if (testerCertificate.isPresent()) {
- operatorCertificates = Stream.concat(operatorCertificates.stream(), testerCertificate.stream()).toList();
- }
- Supplier<Optional<CloudAccount>> cloudAccount = () -> cloudAccountOverride.apply(decideCloudAccountOf(deployment, applicationPackage.truncatedPackage().deploymentSpec()));
- Supplier<DeploymentEndpoints> endpoints = () -> {
- if (preparedEndpoints == null) return DeploymentEndpoints.none;
- PreparedEndpoints prepared = preparedEndpoints.get();
- generatedEndpoints.set(prepared.endpoints().generated());
- return new DeploymentEndpoints(prepared.containerEndpoints(), Optional.of(prepared.certificate()));
- };
- Supplier<List<DataplaneTokenVersions>> dataplaneTokenVersions = () -> {
- Tags tags = applicationPackage.truncatedPackage().deploymentSpec()
- .instance(application.instance())
- .map(DeploymentInstanceSpec::tags)
- .orElse(Tags.empty());
- BasicServicesXml services = applicationPackage.truncatedPackage().services(deployment, tags);
- Set<TokenId> referencedTokens = services.containers().stream()
- .flatMap(container -> container.dataPlaneTokens().stream())
- .collect(toSet());
- List<DataplaneTokenVersions> currentTokens = controller.dataplaneTokenService().listTokens(application.tenant()).stream()
- .filter(token -> referencedTokens.contains(token.tokenId()))
- .toList();
- return Stream.concat(currentTokens.stream(),
- referencedTokens.stream()
- .filter(token -> currentTokens.stream().noneMatch(t -> t.tokenId().equals(token)))
- .map(token -> new DataplaneTokenVersions(token, List.of(), Instant.EPOCH)))
- .toList();
- };
- DeploymentData deploymentData = new DeploymentData(application, zone, applicationPackage::zipStream, platform,
- endpoints, dockerImageRepo, domain, deploymentQuota, tenantSecretStores, operatorCertificates, cloudAccount, dataplaneTokenVersions, dryRun);
- ConfigServer.PreparedApplication preparedApplication = configServer.deploy(deploymentData);
-
- return new DeploymentDataAndResult(deploymentData, preparedApplication.deploymentResult());
- }
- }
-
- private void updateRoutingAndMeta(DeploymentId id, ApplicationPackageStream data, AtomicReference<EndpointList> generatedEndpoints) {
- if (id.applicationId().instance().isTester()) return;
- controller.routing().of(id).activate(data.truncatedPackage().deploymentSpec(), generatedEndpoints.get());
- if ( ! id.zoneId().environment().isManuallyDeployed()) return;
- controller.applications().applicationStore().putMeta(id, clock.instant(), data.truncatedPackage().metaDataZip());
- }
-
- public Optional<CloudAccount> decideCloudAccountOf(DeploymentId deployment, DeploymentSpec spec) {
- ZoneId zoneId = deployment.zoneId();
- CloudName cloud = controller.zoneRegistry().get(zoneId).getCloudName();
- CloudAccount requestedAccount = spec.cloudAccount(cloud, deployment.applicationId().instance(), deployment.zoneId());
- if (requestedAccount.isUnspecified())
- return Optional.empty();
-
- TenantName tenant = deployment.applicationId().tenant();
- Set<CloudAccount> tenantAccounts = accountsOf(tenant);
- if ( ! tenantAccounts.contains(requestedAccount)) {
- throw new IllegalArgumentException("Requested cloud account '" + requestedAccount.value() +
- "' is not valid for tenant '" + tenant + "'");
- }
- if ( ! controller.zoneRegistry().hasZone(zoneId, requestedAccount)) {
- throw new IllegalArgumentException("Zone " + zoneId + " is not configured in requested cloud account '" +
- requestedAccount.value() + "'");
- }
- return Optional.of(requestedAccount);
- }
-
- private LockedApplication withoutDeletedDeployments(LockedApplication application, InstanceName instance) {
- DeploymentSpec deploymentSpec = application.get().deploymentSpec();
- List<ZoneId> deploymentsToRemove = application.get().require(instance).productionDeployments().values().stream()
- .map(Deployment::zone)
- .filter(zone -> deploymentSpec.instance(instance).isEmpty()
- || ! deploymentSpec.requireInstance(instance).deploysTo(zone.environment(),
- zone.region()))
- .toList();
-
- if (deploymentsToRemove.isEmpty())
- return application;
-
- if ( ! application.get().validationOverrides().allows(ValidationId.deploymentRemoval, clock.instant()))
- throw new IllegalArgumentException(ValidationId.deploymentRemoval.value() + ": " + application.get().require(instance) +
- " is deployed in " +
- deploymentsToRemove.stream()
- .map(zone -> zone.region().value())
- .collect(joining(", ")) +
- ", but " + (deploymentsToRemove.size() > 1 ? "these " : "this ") +
- "instance and region combination" +
- (deploymentsToRemove.size() > 1 ? "s are" : " is") +
- " removed from deployment.xml. " +
- ValidationOverrides.toAllowMessage(ValidationId.deploymentRemoval));
- // Remove the instance as well, if it is no longer referenced, and contains only production deployments that are removed now.
- boolean removeInstance = ! deploymentSpec.instanceNames().contains(instance)
- && application.get().require(instance).deployments().size() == deploymentsToRemove.size();
- for (ZoneId zone : deploymentsToRemove) {
- application = deactivate(application.get().id().instance(instance), zone, Optional.of(application)).get();
- }
- if (removeInstance) {
- application = application.without(instance);
- }
- return application;
- }
-
- /**
- * Deletes the given application. All known instances of the applications will be deleted.
- *
- * @throws IllegalArgumentException if the application has deployments or the caller is not authorized
- */
- public void deleteApplication(TenantAndApplicationId id, Credentials credentials) {
- deleteApplication(id, Optional.of(credentials));
- }
-
- public void deleteApplication(TenantAndApplicationId id, Optional<Credentials> credentials) {
- lockApplicationOrThrow(id, application -> {
- var deployments = application.get().instances().values().stream()
- .filter(instance -> ! instance.deployments().isEmpty())
- .collect(toMap(instance -> instance.name(),
- instance -> instance.deployments().keySet().stream()
- .map(ZoneId::toString)
- .collect(joining(", "))));
- if ( ! deployments.isEmpty())
- throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments: " + deployments);
-
- for (Instance instance : application.get().instances().values()) {
- controller.routing().removeRotationEndpointsFromDns(application.get(), instance.name());
- application = application.without(instance.name());
- }
-
- applicationStore.removeAll(id.tenant(), id.application());
- applicationStore.putMetaTombstone(id.tenant(), id.application(), clock.instant());
-
- credentials.ifPresent(creds -> accessControl.deleteApplication(id, creds));
- curator.removeApplication(id);
-
- controller.jobController().collectGarbage();
- controller.notificationsDb().removeNotifications(NotificationSource.from(id));
- log.info("Deleted " + id);
- });
- }
-
- /**
- * Deletes the the given application instance.
- *
- * @throws IllegalArgumentException if the application has deployments or the caller is not authorized
- * @throws NotExistsException if the instance does not exist
- */
- public void deleteInstance(ApplicationId instanceId) {
- if (getInstance(instanceId).isEmpty())
- throw new NotExistsException("Could not delete instance '" + instanceId + "': Instance not found");
-
- lockApplicationOrThrow(TenantAndApplicationId.from(instanceId), application -> {
- if ( ! application.get().require(instanceId.instance()).deployments().isEmpty())
- throw new IllegalArgumentException("Could not delete '" + application + "': It has active deployments in: " +
- application.get().require(instanceId.instance()).deployments().keySet().stream().map(ZoneId::toString)
- .sorted().collect(joining(", ")));
-
- if ( ! application.get().deploymentSpec().equals(DeploymentSpec.empty)
- && application.get().deploymentSpec().instanceNames().contains(instanceId.instance()))
- throw new IllegalArgumentException("Can not delete '" + instanceId + "', which is specified in 'deployment.xml'; remove it there first");
-
- controller.routing().removeRotationEndpointsFromDns(application.get(), instanceId.instance());
- curator.writeApplication(application.without(instanceId.instance()).get());
- controller.jobController().collectGarbage();
- controller.notificationsDb().removeNotifications(NotificationSource.from(instanceId));
- log.info("Deleted " + instanceId);
- });
- }
-
- /**
- * Replace any previous version of this application by this instance
- *
- * @param application a locked application to store
- */
- public void store(LockedApplication application) {
- curator.writeApplication(application.get());
- }
-
- /**
- * Acquire a locked application to modify and store, if there is an application with the given id.
- *
- * @param applicationId ID of the application to lock and get.
- * @param action Function which acts on the locked application.
- */
- public void lockApplicationIfPresent(TenantAndApplicationId applicationId, Consumer<LockedApplication> action) {
- try (Mutex lock = lock(applicationId)) {
- getApplication(applicationId).map(application -> new LockedApplication(application, lock)).ifPresent(action);
- }
- }
-
- /**
- * Acquire a locked application to modify and store, or throw an exception if no application has the given id.
- *
- * @param applicationId ID of the application to lock and require.
- * @param action Function which acts on the locked application.
- * @throws IllegalArgumentException when application does not exist.
- */
- public void lockApplicationOrThrow(TenantAndApplicationId applicationId, Consumer<LockedApplication> action) {
- try (Mutex lock = lock(applicationId)) {
- action.accept(new LockedApplication(requireApplication(applicationId), lock));
- }
- }
-
- /**
- * Tells config server to schedule a restart of all nodes in this deployment
- *
- * @param restartFilter Variables to filter which nodes to restart.
- */
- public void restart(DeploymentId deploymentId, RestartFilter restartFilter) {
- configServer.restart(deploymentId, restartFilter);
- }
-
- /**
- * Asks the config server whether this deployment is currently healthy, i.e., serving traffic as usual.
- * If this cannot be ascertained, we must assume it is not.
- */
- public boolean isHealthy(DeploymentId deploymentId) {
- try {
- return ! isSuspended(deploymentId); // consider adding checks again global routing status, etc.?
- }
- catch (RuntimeException e) {
- log.log(Level.WARNING, "Failed getting suspension status of " + deploymentId + ": " + Exceptions.toMessageString(e));
- return false;
- }
- }
-
- /**
- * Asks the config server whether this deployment is currently <i>suspended</i>:
- * Not in a state where it should receive traffic.
- */
- public boolean isSuspended(DeploymentId deploymentId) {
- return configServer.isSuspended(deploymentId);
- }
-
- /** Sets suspension status of the given deployment in its zone. */
- public void setSuspension(DeploymentId deploymentId, boolean suspend) {
- configServer.setSuspension(deploymentId, suspend);
- }
-
- /** Deactivate application in the given zone. Even if the application itself does not exist, deactivation of the deployment will still be attempted */
- public void deactivate(ApplicationId instanceId, ZoneId zone) {
- TenantAndApplicationId applicationId = TenantAndApplicationId.from(instanceId);
- try (Mutex deploymentLock = lockForDeployment(instanceId, zone)) {
- try (Mutex lock = lock(applicationId)) {
- Optional<LockedApplication> application = getApplication(applicationId).map(app -> new LockedApplication(app, lock));
- deactivate(instanceId, zone, application).ifPresent(this::store);
- }
- }
- }
-
- /**
- * Deactivates a locked application without storing it
- *
- * @return the application with the deployment in the given zone removed
- */
- private Optional<LockedApplication> deactivate(ApplicationId instanceId, ZoneId zone, Optional<LockedApplication> application) {
- DeploymentId id = new DeploymentId(instanceId, zone);
- interface CleanCloseable extends AutoCloseable { void close(); }
- try (CleanCloseable postDeactivation = () -> {
- application.ifPresent(app -> controller.routing().of(id).deactivate(app.get().deploymentSpec()));
- if (id.zoneId().environment().isManuallyDeployed())
- applicationStore.putMetaTombstone(id, clock.instant());
- if ( ! id.zoneId().environment().isTest())
- controller.notificationsDb().removeNotifications(NotificationSource.from(id));
- }) {
- configServer.deactivate(id);
- return application.map(app -> app.with(instanceId.instance(), instance -> instance.withoutDeploymentIn(id.zoneId())));
- }
- }
-
- public DeploymentTrigger deploymentTrigger() { return deploymentTrigger; }
-
- /**
- * Returns a lock which provides exclusive rights to changing this application.
- * Any operation which stores an application need to first acquire this lock, then read, modify
- * and store the application, and finally release (close) the lock.
- */
- Mutex lock(TenantAndApplicationId application) {
- return curator.lock(application);
- }
-
- /**
- * Returns a lock which provides exclusive rights to deploying this application to the given zone.
- */
- private Mutex lockForDeployment(ApplicationId application, ZoneId zone) {
- return curator.lockForDeployment(application, zone);
- }
-
- public VersionCompatibility versionCompatibility(ApplicationId id) {
- return VersionCompatibility.fromVersionList(incompatibleVersions.with(INSTANCE_ID, id.serializedForm()).value());
- }
-
- /**
- * Verifies that the application can be deployed to the tenant, following these rules:
- *
- * 1. Verify that the Athenz service can be launched by the config server
- * 2. If the principal is given, verify that the principal is tenant admin or admin of the tenant domain
- * 3. If the principal is not given, verify that the Athenz domain of the tenant equals Athenz domain given in deployment.xml
- *
- * @param tenantName tenant where application should be deployed
- * @param applicationPackage application package
- * @param deployer principal initiating the deployment, possibly empty
- */
- public void verifyApplicationIdentityConfiguration(TenantName tenantName, Optional<DeploymentId> deployment, ApplicationPackage applicationPackage, Optional<Principal> deployer) {
- Optional<AthenzDomain> identityDomain = applicationPackage.deploymentSpec().athenzDomain()
- .map(domain -> new AthenzDomain(domain.value()));
- if (identityDomain.isEmpty()) {
- // If there is no domain configured in deployment.xml there is nothing to do.
- return;
- }
-
- // Verify that the system supports launching services.
- // Consider adding a capability to the system.
- if ( ! (accessControl instanceof AthenzFacade)) {
- throw new IllegalArgumentException("Athenz domain and service specified in deployment.xml, but not supported by system.");
- }
-
- // Verify that the config server is allowed to launch the service specified
- verifyAllowedLaunchAthenzService(applicationPackage.deploymentSpec());
-
- // If a user principal is initiating the request, verify that the user is allowed to launch the service.
- // Either the user is member of the domain admin role, or is given the "launch" privilege on the service.
- Optional<AthenzUser> athenzUser = getUser(deployer);
- if (athenzUser.isPresent()) {
- // This is a direct deployment, and we need only validate what the configserver will actually launch.
- DeploymentId id = deployment.orElseThrow(() -> new IllegalArgumentException("Unable to evaluate access, no zone provided in deployment"));
- var serviceToLaunch = applicationPackage.deploymentSpec().athenzService(id.applicationId().instance(),
- id.zoneId().environment(),
- id.zoneId().region())
- .map(service -> new AthenzService(identityDomain.get(), service.value()));
- if (serviceToLaunch.isPresent()) {
- if (
- ! ((AthenzFacade) accessControl).canLaunch(athenzUser.get(), serviceToLaunch.get()) && // launch privilege
- ! ((AthenzFacade) accessControl).hasTenantAdminAccess(athenzUser.get(), identityDomain.get()) // tenant admin
- ) {
- throw new IllegalArgumentException("User " + athenzUser.get().getFullName() + " is not allowed to launch " +
- "service " + serviceToLaunch.get().getFullName() + ". " +
- "Please reach out to the domain admin.");
- }
- } else {
- // This is a rare edge case where deployment.xml specifies athenz-service on each step, but not on the root.
- // It is undefined which service should be launched, so handle this as an error.
- throw new IllegalArgumentException("Athenz domain configured, but no service defined for deployment to " + id.zoneId().value());
- }
- } else {
- // If this is a deployment pipeline, verify that the domain in deployment.xml is the same as the tenant domain. Access control is already validated before this step.
- Tenant tenant = controller.tenants().require(tenantName);
- AthenzDomain tenantDomain = ((AthenzTenant) tenant).domain();
- if ( ! Objects.equals(tenantDomain, identityDomain.get()))
- throw new IllegalArgumentException("Athenz domain in deployment.xml: [" + identityDomain.get().getName() + "] " +
- "must match tenant domain: [" + tenantDomain.getName() + "]");
- }
- }
-
- private TenantAndApplicationId dashToUnderscore(TenantAndApplicationId id) {
- return TenantAndApplicationId.from(id.tenant().value(), id.application().value().replaceAll("-", "_"));
- }
-
- private ApplicationId dashToUnderscore(ApplicationId id) {
- return dashToUnderscore(TenantAndApplicationId.from(id)).instance(id.instance());
- }
-
- private QuotaUsage deploymentQuotaUsage(ZoneId zoneId, ApplicationId applicationId) {
- var application = configServer.nodeRepository().getApplication(zoneId, applicationId);
- return DeploymentQuotaCalculator.calculateQuotaUsage(application);
- }
-
- /*
- * Get the AthenzUser from this principal or Optional.empty if this does not represent a user.
- */
- private Optional<AthenzUser> getUser(Optional<Principal> deployer) {
- return deployer
- .filter(AthenzPrincipal.class::isInstance)
- .map(AthenzPrincipal.class::cast)
- .map(AthenzPrincipal::getIdentity)
- .filter(AthenzUser.class::isInstance)
- .map(AthenzUser.class::cast);
- }
-
- /*
- * Verifies that the configured athenz service (if any) can be launched.
- */
- private void verifyAllowedLaunchAthenzService(DeploymentSpec deploymentSpec) {
- deploymentSpec.athenzDomain().ifPresent(domain -> {
- controller.zoneRegistry().zones().reachable().ids().forEach(zone -> {
- AthenzIdentity configServerAthenzIdentity = controller.zoneRegistry().getConfigServerHttpsIdentity(zone);
- deploymentSpec.athenzService().ifPresent(service -> {
- verifyAthenzServiceCanBeLaunchedBy(configServerAthenzIdentity, new AthenzService(domain.value(), service.value()));
- });
- deploymentSpec.instances().forEach(spec -> {
- spec.athenzService(zone.environment(), zone.region()).ifPresent(service -> {
- verifyAthenzServiceCanBeLaunchedBy(configServerAthenzIdentity, new AthenzService(domain.value(), service.value()));
- });
- });
- });
- });
- }
-
- private void verifyAthenzServiceCanBeLaunchedBy(AthenzIdentity configServerAthenzIdentity, AthenzService athenzService) {
- if ( ! ((AthenzFacade) accessControl).canLaunch(configServerAthenzIdentity, athenzService))
- throw new IllegalArgumentException("Not allowed to launch Athenz service " + athenzService.getFullName());
- }
-
- /** Extract deployment warnings metric from deployment result */
- private static Map<DeploymentMetrics.Warning, Integer> warningsFrom(List<DeploymentResult.LogEntry> log) {
- return log.stream()
- .filter(entry -> entry.level().intValue() >= Level.WARNING.intValue())
- // TODO: Categorize warnings. Response from config server should be updated to include the appropriate
- // category and typed log level
- .collect(groupingBy(__ -> Warning.all,
- collectingAndThen(counting(), Long::intValue)));
- }
-
- public void verifyPlan(TenantName tenantName) {
- var planId = controller.serviceRegistry().billingController().getPlan(tenantName);
- Optional<Plan> plan = controller.serviceRegistry().planRegistry().plan(planId);
- if (plan.isEmpty())
- throw new IllegalArgumentException("Tenant '" + tenantName.value() + "' has no plan, not allowed to deploy. See https://cloud.vespa.ai/support");
- if (plan.get().quota().calculate().equals(Quota.zero()))
- throw new IllegalArgumentException("Tenant '" + tenantName.value() + "' has a plan '" +
- plan.get().displayName() + "' with zero quota, not allowed to deploy. See https://cloud.vespa.ai/support");
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
deleted file mode 100644
index 0b693bb9894..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Controller.java
+++ /dev/null
@@ -1,306 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.component.Version;
-import com.yahoo.component.Vtag;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.concurrent.maintenance.JobControl;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.container.jdisc.secretstore.SecretStore;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.maven.MavenRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.application.MailVerifier;
-import com.yahoo.vespa.hosted.controller.archive.CuratorArchiveBucketDb;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLogger;
-import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
-import com.yahoo.vespa.hosted.controller.deployment.JobController;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceForwarder;
-import com.yahoo.vespa.hosted.controller.notification.NotificationsDb;
-import com.yahoo.vespa.hosted.controller.notification.Notifier;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.persistence.JobControlFlags;
-import com.yahoo.vespa.hosted.controller.restapi.dataplanetoken.DataplaneTokenService;
-import com.yahoo.vespa.hosted.controller.security.AccessControl;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccessControl;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-import com.yahoo.yolean.concurrent.Sleeper;
-
-import java.security.SecureRandom;
-import java.time.Clock;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Random;
-import java.util.Set;
-import java.util.function.Predicate;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-import static java.util.stream.Collectors.toSet;
-
-/**
- * API to the controller. This contains the object model of everything the controller cares about, mainly tenants and
- * applications. The object model is persisted to curator.
- *
- * All the individual model objects reachable from the Controller are immutable.
- *
- * Access to the controller is multi-thread safe, provided the locking methods are
- * used when accessing, modifying and storing objects provided by the controller.
- *
- * @author bratseth
- */
-public class Controller extends AbstractComponent {
-
- private static final Logger log = Logger.getLogger(Controller.class.getName());
-
- private final CuratorDb curator;
- private final JobControl jobControl;
- private final ApplicationController applicationController;
- private final TenantController tenantController;
- private final JobController jobController;
- private final Clock clock;
- private final Sleeper sleeper;
- private final ZoneRegistry zoneRegistry;
- private final ServiceRegistry serviceRegistry;
- private final AuditLogger auditLogger;
- private final FlagSource flagSource;
- private final NameServiceForwarder nameServiceForwarder;
- private final MavenRepository mavenRepository;
- private final Metric metric;
- private final RoutingController routingController;
- private final OsController osController;
- private final ControllerConfig controllerConfig;
- private final SecretStore secretStore;
- private final CuratorArchiveBucketDb archiveBucketDb;
- private final NotificationsDb notificationsDb;
- private final SupportAccessControl supportAccessControl;
- private final Notifier notifier;
- private final MailVerifier mailVerifier;
- private final DataplaneTokenService dataplaneTokenService;
- private final Random random;
- private final Random secureRandom; // Type is Random to allow for test determinism
-
- /**
- * Creates a controller
- *
- * @param curator the curator instance storing the persistent state of the controller.
- */
- @Inject
- public Controller(CuratorDb curator, RotationsConfig rotationsConfig, AccessControl accessControl, FlagSource flagSource,
- MavenRepository mavenRepository, ServiceRegistry serviceRegistry, Metric metric, SecretStore secretStore,
- ControllerConfig controllerConfig) {
- this(curator, rotationsConfig, accessControl, flagSource,
- mavenRepository, serviceRegistry, metric, secretStore, controllerConfig, Sleeper.DEFAULT, new Random(),
- new SecureRandom());
- }
-
- public Controller(CuratorDb curator, RotationsConfig rotationsConfig, AccessControl accessControl,
- FlagSource flagSource, MavenRepository mavenRepository,
- ServiceRegistry serviceRegistry, Metric metric, SecretStore secretStore,
- ControllerConfig controllerConfig, Sleeper sleeper, Random random, Random secureRandom) {
- this.curator = Objects.requireNonNull(curator, "Curator cannot be null");
- this.serviceRegistry = Objects.requireNonNull(serviceRegistry, "ServiceRegistry cannot be null");
- this.zoneRegistry = Objects.requireNonNull(serviceRegistry.zoneRegistry(), "ZoneRegistry cannot be null");
- this.clock = Objects.requireNonNull(serviceRegistry.clock(), "Clock cannot be null");
- this.sleeper = Objects.requireNonNull(sleeper, "Sleeper cannot be null");
- this.flagSource = Objects.requireNonNull(flagSource, "FlagSource cannot be null");
- this.mavenRepository = Objects.requireNonNull(mavenRepository, "MavenRepository cannot be null");
- this.metric = Objects.requireNonNull(metric, "Metric cannot be null");
- this.controllerConfig = Objects.requireNonNull(controllerConfig, "ControllerConfig cannot be null");
- this.secretStore = Objects.requireNonNull(secretStore, "SecretStore cannot be null");
- this.random = Objects.requireNonNull(random, "Random cannot be null");
- this.secureRandom = Objects.requireNonNull(secureRandom, "SecureRandom cannot be null");
-
- nameServiceForwarder = new NameServiceForwarder(curator);
- jobController = new JobController(this);
- applicationController = new ApplicationController(this, curator, accessControl, clock, flagSource, serviceRegistry.billingController());
- tenantController = new TenantController(this, curator, accessControl);
- routingController = new RoutingController(this, rotationsConfig);
- osController = new OsController(this);
- auditLogger = new AuditLogger(curator, clock);
- jobControl = new JobControl(new JobControlFlags(curator, flagSource));
- archiveBucketDb = new CuratorArchiveBucketDb(this);
- notifier = new Notifier(curator, serviceRegistry.consoleUrls(), serviceRegistry.mailer(), flagSource);
- notificationsDb = new NotificationsDb(this);
- supportAccessControl = new SupportAccessControl(this);
- mailVerifier = new MailVerifier(serviceRegistry.consoleUrls(), tenantController, serviceRegistry.mailer(), curator, clock);
- dataplaneTokenService = new DataplaneTokenService(this);
-
- // Record the version of this controller
- curator().writeControllerVersion(this.hostname(), serviceRegistry.controllerVersion());
-
- jobController.updateStorage();
- }
-
- /** Returns the instance controlling tenants */
- public TenantController tenants() { return tenantController; }
-
- /** Returns the instance controlling applications */
- public ApplicationController applications() { return applicationController; }
-
- /** Returns the instance controlling deployment jobs. */
- public JobController jobController() { return jobController; }
-
- /** Returns the instance controlling routing */
- public RoutingController routing() {
- return routingController;
- }
-
- /** Returns the instance controlling OS upgrades */
- public OsController os() {
- return osController;
- }
-
- /** Returns the service registry of this */
- public ServiceRegistry serviceRegistry() {
- return serviceRegistry;
- }
-
- /** Provides access to the feature flags of this */
- public FlagSource flagSource() {
- return flagSource;
- }
-
- public Clock clock() { return clock; }
-
- public Sleeper sleeper() { return sleeper; }
-
- public ZoneRegistry zoneRegistry() { return zoneRegistry; }
-
- public NameServiceForwarder nameServiceForwarder() { return nameServiceForwarder; }
-
- public MavenRepository mavenRepository() { return mavenRepository; }
-
- public ControllerConfig controllerConfig() { return controllerConfig; }
-
- /** Replace the current version status by a new one */
- public void updateVersionStatus(VersionStatus newStatus) {
- VersionStatus currentStatus = readVersionStatus();
- if (newStatus.systemVersion().isPresent() &&
- ! newStatus.systemVersion().equals(currentStatus.systemVersion())) {
- log.info("Changing system version from " + printableVersion(currentStatus.systemVersion()) +
- " to " + printableVersion(newStatus.systemVersion()));
- }
- Set<Version> obsoleteVersions = currentStatus.versions().stream().map(VespaVersion::versionNumber).collect(toSet());
- for (VespaVersion version : newStatus.versions()) {
- obsoleteVersions.remove(version.versionNumber());
- VespaVersion current = currentStatus.version(version.versionNumber());
- if (current == null)
- log.info("New version " + version.versionNumber().toFullString() + " added");
- else if ( ! current.confidence().equals(version.confidence()))
- log.info("Confidence for version " + version.versionNumber().toFullString() +
- " changed from " + current.confidence() + " to " + version.confidence());
- }
- for (Version version : obsoleteVersions)
- log.info("Version " + version.toFullString() + " is obsolete, and will be forgotten");
-
- curator.writeVersionStatus(newStatus);
- removeConfidenceOverride(obsoleteVersions::contains);
- }
-
- /** Returns the latest known version status. Calling this is free but the status may be slightly out of date. */
- public VersionStatus readVersionStatus() { return curator.readVersionStatus(); }
-
- /** Remove confidence override for versions matching given filter */
- public void removeConfidenceOverride(Predicate<Version> filter) {
- try (Mutex lock = curator.lockConfidenceOverrides()) {
- Map<Version, VespaVersion.Confidence> overrides = new LinkedHashMap<>(curator.readConfidenceOverrides());
- overrides.keySet().removeIf(filter);
- curator.writeConfidenceOverrides(overrides);
- }
- }
-
- /** Returns the current system version: The controller should drive towards running all applications on this version */
- public Version readSystemVersion() {
- return systemVersion(readVersionStatus());
- }
-
- /** Returns the current system version from given status: The controller should drive towards running all applications on this version */
- public Version systemVersion(VersionStatus versionStatus) {
- return versionStatus.systemVersion()
- .map(VespaVersion::versionNumber)
- .orElse(Vtag.currentVersion);
- }
-
- /** Returns the hostname of this controller */
- public HostName hostname() {
- return serviceRegistry.getHostname();
- }
-
- public SystemName system() {
- return zoneRegistry.system();
- }
-
- public CuratorDb curator() {
- return curator;
- }
-
- public AuditLogger auditLogger() {
- return auditLogger;
- }
-
- public Metric metric() {
- return metric;
- }
-
- public SecretStore secretStore() {
- return secretStore;
- }
-
- /** Clouds present in this system */
- public Set<CloudName> clouds() {
- return zoneRegistry.zones().all().zones().stream()
- .map(ZoneApi::getCloudName)
- .collect(Collectors.toUnmodifiableSet());
- }
-
- private static String printableVersion(Optional<VespaVersion> vespaVersion) {
- return vespaVersion.map(v -> v.versionNumber().toFullString()).orElse("unknown");
- }
-
- public JobControl jobControl() {
- return jobControl;
- }
-
- public CuratorArchiveBucketDb archiveBucketDb() {
- return archiveBucketDb;
- }
-
- public NotificationsDb notificationsDb() {
- return notificationsDb;
- }
-
- public SupportAccessControl supportAccess() {
- return supportAccessControl;
- }
-
- public Notifier notifier() {
- return notifier;
- }
-
- public MailVerifier mailVerifier() {
- return mailVerifier;
- }
-
- public DataplaneTokenService dataplaneTokenService() {
- return dataplaneTokenService;
- }
-
- /** Returns a random number generator. If secure is true, this returns a {@link SecureRandom} suitable for
- * cryptographic purposes */
- public Random random(boolean secure) {
- return secure ? secureRandom : random;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
deleted file mode 100644
index 0a9c680251c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/Instance.java
+++ /dev/null
@@ -1,235 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentActivity;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationStatus;
-
-import java.time.Instant;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalDouble;
-import java.util.OptionalLong;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import static java.util.Comparator.naturalOrder;
-
-/**
- * An instance of an application.
- *
- * This is immutable.
- *
- * @author bratseth
- */
-public class Instance {
-
- private final ApplicationId id;
- private final Map<ZoneId, Deployment> deployments;
- private final List<AssignedRotation> rotations;
- private final RotationStatus rotationStatus;
- private final Map<JobType, Instant> jobPauses;
- private final Change change;
-
- /** Creates an empty instance */
- public Instance(ApplicationId id) {
- this(id, Set.of(), Map.of(), List.of(), RotationStatus.EMPTY, Change.empty());
- }
-
- /** Creates an empty instance*/
- public Instance(ApplicationId id, Collection<Deployment> deployments, Map<JobType, Instant> jobPauses,
- List<AssignedRotation> rotations, RotationStatus rotationStatus, Change change) {
- this.id = Objects.requireNonNull(id, "id cannot be null");
- this.deployments = Objects.requireNonNull(deployments, "deployments cannot be null").stream()
- .collect(Collectors.toUnmodifiableMap(Deployment::zone, Function.identity()));
- this.jobPauses = Map.copyOf(Objects.requireNonNull(jobPauses, "deploymentJobs cannot be null"));
- this.rotations = List.copyOf(Objects.requireNonNull(rotations, "rotations cannot be null"));
- this.rotationStatus = Objects.requireNonNull(rotationStatus, "rotationStatus cannot be null");
- this.change = Objects.requireNonNull(change, "change cannot be null");
- }
-
- public Instance withNewDeployment(ZoneId zone, RevisionId revision, Version version, Instant instant,
- Map<DeploymentMetrics.Warning, Integer> warnings, QuotaUsage quotaUsage, CloudAccount cloudAccount,
- List<DataplaneTokenVersions> dataPlaneTokens) {
- Map<TokenId, Instant> dataPlaneTokenIds = dataPlaneTokens.stream().collect(Collectors.toMap(token -> token.tokenId(),
- token -> token.lastUpdated()));
- // Use info from previous deployment if available, otherwise create a new one.
- Deployment previousDeployment = deployments.getOrDefault(zone, new Deployment(zone, cloudAccount, revision,
- version, instant,
- DeploymentMetrics.none,
- DeploymentActivity.none,
- QuotaUsage.none,
- OptionalDouble.empty(),
- dataPlaneTokenIds));
- Deployment newDeployment = new Deployment(zone, cloudAccount, revision, version, instant,
- previousDeployment.metrics().with(warnings),
- previousDeployment.activity(),
- quotaUsage,
- previousDeployment.cost(),
- dataPlaneTokenIds);
- return with(newDeployment);
- }
-
- public Instance withJobPause(JobType jobType, OptionalLong pausedUntil) {
- Map<JobType, Instant> jobPauses = new HashMap<>(this.jobPauses);
- if (pausedUntil.isPresent())
- jobPauses.put(jobType, Instant.ofEpochMilli(pausedUntil.getAsLong()));
- else
- jobPauses.remove(jobType);
-
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
- }
-
- public Instance recordActivityAt(Instant instant, ZoneId zone) {
- Deployment deployment = deployments.get(zone);
- if (deployment == null) return this;
- return with(deployment.recordActivityAt(instant));
- }
-
- public Instance with(ZoneId zone, DeploymentMetrics deploymentMetrics) {
- Deployment deployment = deployments.get(zone);
- if (deployment == null) return this; // No longer deployed in this zone.
- return with(deployment.withMetrics(deploymentMetrics));
- }
-
- public Instance withDeploymentCosts(Map<ZoneId, Double> costByZone) {
- Map<ZoneId, Deployment> deployments = this.deployments.entrySet().stream()
- .map(entry -> Optional.ofNullable(costByZone.get(entry.getKey()))
- .map(entry.getValue()::withCost)
- .orElseGet(entry.getValue()::withoutCost))
- .collect(Collectors.toUnmodifiableMap(Deployment::zone, deployment -> deployment));
- return with(deployments);
- }
-
- public Instance withoutDeploymentIn(ZoneId zone) {
- Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(this.deployments);
- deployments.remove(zone);
- return with(deployments);
- }
-
- public Instance with(List<AssignedRotation> assignedRotations) {
- return new Instance(id, deployments.values(), jobPauses, assignedRotations, rotationStatus, change);
- }
-
- public Instance with(RotationStatus rotationStatus) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
- }
-
- public Instance withChange(Change change) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
- }
-
- private Instance with(Deployment deployment) {
- Map<ZoneId, Deployment> deployments = new LinkedHashMap<>(this.deployments);
- deployments.put(deployment.zone(), deployment);
- return with(deployments);
- }
-
- private Instance with(Map<ZoneId, Deployment> deployments) {
- return new Instance(id, deployments.values(), jobPauses, rotations, rotationStatus, change);
- }
-
- public ApplicationId id() { return id; }
-
- public InstanceName name() { return id.instance(); }
-
- /** Returns an immutable map of the current deployments of this */
- public Map<ZoneId, Deployment> deployments() { return deployments; }
-
- /**
- * Returns an immutable map of the current *production* deployments of this
- * (deployments also includes manually deployed environments)
- */
- public Map<ZoneId, Deployment> productionDeployments() {
- return deployments.values().stream()
- .filter(deployment -> deployment.zone().environment() == Environment.prod)
- .collect(Collectors.toUnmodifiableMap(Deployment::zone, Function.identity()));
- }
-
- /** Returns the instant until which the given job is paused, or empty. */
- public Optional<Instant> jobPause(JobType jobType) {
- return Optional.ofNullable(jobPauses.get(jobType));
- }
-
- /** Returns the set of instants until which any paused jobs of this instance should remain paused, indexed by job type. */
- public Map<JobType, Instant> jobPauses() {
- return jobPauses;
- }
-
- /** Returns all rotations assigned to this */
- public List<AssignedRotation> rotations() {
- return rotations;
- }
-
- /** Returns the status of the global rotation(s) assigned to this */
- public RotationStatus rotationStatus() {
- return rotationStatus;
- }
-
- /** Returns the currently deploying change for this instance. */
- public Change change() {
- return change;
- }
-
- /** Returns the total quota usage for this instance, excluding temporary deployments **/
- public QuotaUsage quotaUsage() {
- return deployments.values().stream()
- .filter(d -> !d.zone().environment().isTest()) // Exclude temporary deployments
- .map(Deployment::quota).reduce(QuotaUsage::add).orElse(QuotaUsage.none);
- }
-
- /** Returns the total quota usage for manual deployments for this instance **/
- public QuotaUsage manualQuotaUsage() {
- return deployments.values().stream()
- .filter(d -> d.zone().environment().isManuallyDeployed())
- .map(Deployment::quota).reduce(QuotaUsage::add).orElse(QuotaUsage.none);
- }
-
- /** Returns the total quota usage for this instance, excluding one specific deployment (and temporary deployments) */
- public QuotaUsage quotaUsageExcluding(ApplicationId application, ZoneId zone) {
- return deployments.values().stream()
- .filter(d -> !d.zone().environment().isTest()) // Exclude temporary deployments
- .filter(d -> !(application.equals(id) && d.zone().equals(zone)))
- .map(Deployment::quota).reduce(QuotaUsage::add).orElse(QuotaUsage.none);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if ( ! (o instanceof Instance)) return false;
-
- Instance that = (Instance) o;
-
- return id.equals(that.id);
- }
-
- @Override
- public int hashCode() {
- return id.hashCode();
- }
-
- @Override
- public String toString() {
- return "application instance '" + id.toFullString() + "'";
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
deleted file mode 100644
index 830e40bd638..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedApplication.java
+++ /dev/null
@@ -1,193 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.AccountId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
-import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
-
-import java.security.PublicKey;
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.LinkedHashSet;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.OptionalLong;
-import java.util.Set;
-import java.util.function.UnaryOperator;
-
-/**
- * An application that has been locked for modification. Provides methods for modifying an application's fields.
- *
- * @author jonmv
- */
-public class LockedApplication {
-
- private final Mutex lock;
- private final TenantAndApplicationId id;
- private final Instant createdAt;
- private final DeploymentSpec deploymentSpec;
- private final ValidationOverrides validationOverrides;
- private final Optional<IssueId> deploymentIssueId;
- private final Optional<IssueId> ownershipIssueId;
- private final Optional<User> userOwner;
- private final Optional<AccountId> issueOwner;
- private final OptionalInt majorVersion;
- private final ApplicationMetrics metrics;
- private final Set<PublicKey> deployKeys;
- private final OptionalLong projectId;
- private final RevisionHistory revisions;
- private final Map<InstanceName, Instance> instances;
-
- /**
- * Used to create a locked application
- *
- * @param application The application to lock.
- * @param lock The lock for the application.
- */
- LockedApplication(Application application, Mutex lock) {
- this(Objects.requireNonNull(lock, "lock cannot be null"), application.id(), application.createdAt(),
- application.deploymentSpec(), application.validationOverrides(), application.deploymentIssueId(), application.ownershipIssueId(),
- application.userOwner(), application.issueOwner(), application.majorVersion(), application.metrics(), application.deployKeys(),
- application.projectId(), application.instances(), application.revisions());
- }
-
- private LockedApplication(Mutex lock, TenantAndApplicationId id, Instant createdAt, DeploymentSpec deploymentSpec,
- ValidationOverrides validationOverrides, Optional<IssueId> deploymentIssueId,
- Optional<IssueId> ownershipIssueId, Optional<User> userOwner, Optional<AccountId> issueOwner,
- OptionalInt majorVersion, ApplicationMetrics metrics, Set<PublicKey> deployKeys,
- OptionalLong projectId, Map<InstanceName, Instance> instances, RevisionHistory revisions) {
- this.lock = lock;
- this.id = id;
- this.createdAt = createdAt;
- this.deploymentSpec = deploymentSpec;
- this.validationOverrides = validationOverrides;
- this.deploymentIssueId = deploymentIssueId;
- this.ownershipIssueId = ownershipIssueId;
- this.userOwner = userOwner;
- this.issueOwner = issueOwner;
- this.majorVersion = majorVersion;
- this.metrics = metrics;
- this.deployKeys = deployKeys;
- this.projectId = projectId;
- this.revisions = revisions;
- this.instances = Map.copyOf(instances);
- }
-
- /** Returns a read-only copy of this */
- public Application get() {
- return new Application(id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, deployKeys,
- projectId, revisions, instances.values());
- }
-
- LockedApplication withNewInstance(InstanceName instance) {
- var instances = new HashMap<>(this.instances);
- instances.put(instance, new Instance(id.instance(instance)));
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, deployKeys,
- projectId, instances, revisions);
- }
-
- public LockedApplication with(InstanceName instance, UnaryOperator<Instance> modification) {
- var instances = new HashMap<>(this.instances);
- instances.put(instance, modification.apply(instances.get(instance)));
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, deployKeys,
- projectId, instances, revisions);
- }
-
- public LockedApplication without(InstanceName instance) {
- var instances = new HashMap<>(this.instances);
- instances.remove(instance);
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, deployKeys,
- projectId, instances, revisions);
- }
-
- public LockedApplication withProjectId(OptionalLong projectId) {
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, deployKeys,
- projectId, instances, revisions);
- }
-
- public LockedApplication withDeploymentIssueId(IssueId issueId) {
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- Optional.ofNullable(issueId), ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, deployKeys,
- projectId, instances, revisions);
- }
-
- public LockedApplication with(DeploymentSpec deploymentSpec) {
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, deployKeys,
- projectId, instances, revisions);
- }
-
- public LockedApplication with(ValidationOverrides validationOverrides) {
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, deployKeys,
- projectId, instances, revisions);
- }
-
- public LockedApplication withOwnershipIssueId(IssueId issueId) {
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, Optional.of(issueId), userOwner, issueOwner, majorVersion, metrics, deployKeys,
- projectId, instances, revisions);
- }
-
- public LockedApplication withOwner(AccountId issueOwner) {
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, Optional.of(issueOwner), majorVersion, metrics, deployKeys,
- projectId, instances, revisions);
- }
-
- /** Set a major version for this, or set to null to remove any major version override */
- public LockedApplication withMajorVersion(Integer majorVersion) {
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner,
- issueOwner, majorVersion == null ? OptionalInt.empty() : OptionalInt.of(majorVersion),
- metrics, deployKeys, projectId, instances, revisions);
- }
-
- public LockedApplication with(ApplicationMetrics metrics) {
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, deployKeys,
- projectId, instances, revisions);
- }
-
- public LockedApplication withDeployKey(PublicKey pemDeployKey) {
- Set<PublicKey> keys = new LinkedHashSet<>(deployKeys);
- keys.add(pemDeployKey);
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, keys,
- projectId, instances, revisions);
- }
-
- public LockedApplication withoutDeployKey(PublicKey pemDeployKey) {
- Set<PublicKey> keys = new LinkedHashSet<>(deployKeys);
- keys.remove(pemDeployKey);
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics, keys,
- projectId, instances, revisions);
- }
-
- public LockedApplication withRevisions(UnaryOperator<RevisionHistory> change) {
- return new LockedApplication(lock, id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics,
- deployKeys, projectId, instances, change.apply(revisions));
- }
-
- @Override
- public String toString() {
- return "application '" + id + "'";
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
deleted file mode 100644
index bfba17bef22..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/LockedTenant.java
+++ /dev/null
@@ -1,308 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
-import com.google.common.collect.ImmutableBiMap;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.BillingReference;
-import com.yahoo.vespa.hosted.controller.tenant.CloudAccountInfo;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-
-import java.security.Principal;
-import java.security.PublicKey;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * A tenant that has been locked for modification. Provides methods for modifying a tenant's fields.
- *
- * @author mpolden
- * @author jonmv
- */
-public abstract class LockedTenant {
-
- final TenantName name;
- final Instant createdAt;
- final LastLoginInfo lastLoginInfo;
- final Instant tenantRolesLastMaintained;
- final List<CloudAccountInfo> cloudAccounts;
-
- private LockedTenant(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Instant tenantRolesLastMaintained, List<CloudAccountInfo> cloudAccounts) {
- this.name = requireNonNull(name);
- this.createdAt = requireNonNull(createdAt);
- this.lastLoginInfo = requireNonNull(lastLoginInfo);
- this.tenantRolesLastMaintained = requireNonNull(tenantRolesLastMaintained);
- this.cloudAccounts = requireNonNull(cloudAccounts);
- }
-
- static LockedTenant of(Tenant tenant, Mutex lock) {
- return switch (tenant.type()) {
- case athenz -> new Athenz((AthenzTenant) tenant);
- case cloud -> new Cloud((CloudTenant) tenant);
- case deleted -> new Deleted((DeletedTenant) tenant);
- };
- }
-
- /** Returns a read-only copy of this */
- public abstract Tenant get();
-
- public abstract LockedTenant with(LastLoginInfo lastLoginInfo);
-
- public abstract LockedTenant with(Instant tenantRolesLastMaintained);
-
- public abstract LockedTenant withCloudAccounts(List<CloudAccountInfo> cloudAccounts);
-
- public Deleted deleted(Instant deletedAt) {
- return new Deleted(new DeletedTenant(name, createdAt, deletedAt));
- }
-
- @Override
- public String toString() {
- return "tenant '" + name + "'";
- }
-
-
- /** A locked AthenzTenant. */
- public static class Athenz extends LockedTenant {
-
- private final AthenzDomain domain;
- private final Property property;
- private final Optional<PropertyId> propertyId;
- private final Optional<Contact> contact;
-
- private Athenz(TenantName name, AthenzDomain domain, Property property, Optional<PropertyId> propertyId,
- Optional<Contact> contact, Instant createdAt, LastLoginInfo lastLoginInfo, Instant tenantRolesLastMaintained, List<CloudAccountInfo> cloudAccounts) {
- super(name, createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccounts);
- this.domain = domain;
- this.property = property;
- this.propertyId = propertyId;
- this.contact = contact;
- }
-
- private Athenz(AthenzTenant tenant) {
- this(tenant.name(), tenant.domain(), tenant.property(), tenant.propertyId(), tenant.contact(), tenant.createdAt(), tenant.lastLoginInfo(), tenant.tenantRolesLastMaintained(), tenant.cloudAccounts());
- }
-
- @Override
- public AthenzTenant get() {
- return new AthenzTenant(name, domain, property, propertyId, contact, createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccounts);
- }
-
- public Athenz with(AthenzDomain domain) {
- return new Athenz(name, domain, property, propertyId, contact, createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccounts);
- }
-
- public Athenz with(Property property) {
- return new Athenz(name, domain, property, propertyId, contact, createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccounts);
- }
-
- public Athenz with(PropertyId propertyId) {
- return new Athenz(name, domain, property, Optional.of(propertyId), contact, createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccounts);
- }
-
- public Athenz with(Contact contact) {
- return new Athenz(name, domain, property, propertyId, Optional.of(contact), createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccounts);
- }
-
- @Override
- public LockedTenant with(LastLoginInfo lastLoginInfo) {
- return new Athenz(name, domain, property, propertyId, contact, createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccounts);
- }
-
- @Override
- public LockedTenant with(Instant tenantRolesLastMaintained) {
- return new Athenz(name, domain, property, propertyId, contact, createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccounts);
- }
-
- @Override
- public LockedTenant withCloudAccounts(List<CloudAccountInfo> cloudAccounts) {
- return new Athenz(name, domain, property, propertyId, contact, createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccounts);
- }
-
- }
-
-
- /** A locked CloudTenant. */
- public static class Cloud extends LockedTenant {
-
- private final Optional<SimplePrincipal> creator;
- private final BiMap<PublicKey, SimplePrincipal> developerKeys;
- private final TenantInfo info;
- private final List<TenantSecretStore> tenantSecretStores;
- private final ArchiveAccess archiveAccess;
- private final Optional<Instant> invalidateUserSessionsBefore;
- private final Optional<BillingReference> billingReference;
- private final PlanId planId;
-
- private Cloud(TenantName name, Instant createdAt, LastLoginInfo lastLoginInfo, Optional<SimplePrincipal> creator,
- BiMap<PublicKey, SimplePrincipal> developerKeys, TenantInfo info,
- List<TenantSecretStore> tenantSecretStores, ArchiveAccess archiveAccess,
- Optional<Instant> invalidateUserSessionsBefore, Instant tenantRolesLastMaintained,
- List<CloudAccountInfo> cloudAccounts, Optional<BillingReference> billingReference,
- PlanId planId) {
- super(name, createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccounts);
- this.developerKeys = ImmutableBiMap.copyOf(developerKeys);
- this.creator = creator;
- this.info = info;
- this.tenantSecretStores = tenantSecretStores;
- this.archiveAccess = archiveAccess;
- this.invalidateUserSessionsBefore = invalidateUserSessionsBefore;
- this.billingReference = billingReference;
- this.planId = planId;
- }
-
- private Cloud(CloudTenant tenant) {
- this(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), tenant.creator(), tenant.developerKeys(),
- tenant.info(), tenant.tenantSecretStores(), tenant.archiveAccess(), tenant.invalidateUserSessionsBefore(),
- tenant.tenantRolesLastMaintained(), tenant.cloudAccounts(), tenant.billingReference(), tenant.planId());
- }
-
- @Override
- public CloudTenant get() {
- return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores,
- archiveAccess, invalidateUserSessionsBefore, tenantRolesLastMaintained,
- cloudAccounts, billingReference, planId);
- }
-
- public Cloud withDeveloperKey(PublicKey key, Principal principal) {
- BiMap<PublicKey, SimplePrincipal> keys = HashBiMap.create(developerKeys);
- SimplePrincipal simplePrincipal = new SimplePrincipal(principal.getName());
- if (keys.containsKey(key))
- throw new IllegalArgumentException("Key " + KeyUtils.toPem(key) + " is already owned by " + keys.get(key));
- if (keys.inverse().containsKey(simplePrincipal))
- throw new IllegalArgumentException(principal + " is already associated with key " + KeyUtils.toPem(keys.inverse().get(simplePrincipal)));
- keys.put(key, simplePrincipal);
- return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccess,
- invalidateUserSessionsBefore, tenantRolesLastMaintained, cloudAccounts,
- billingReference, planId);
- }
-
- public Cloud withoutDeveloperKey(PublicKey key) {
- BiMap<PublicKey, SimplePrincipal> keys = HashBiMap.create(developerKeys);
- keys.remove(key);
- return new Cloud(name, createdAt, lastLoginInfo, creator, keys, info, tenantSecretStores, archiveAccess,
- invalidateUserSessionsBefore, tenantRolesLastMaintained, cloudAccounts, billingReference,
- planId);
- }
-
- public Cloud withInfo(TenantInfo newInfo) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, newInfo, tenantSecretStores,
- archiveAccess, invalidateUserSessionsBefore, tenantRolesLastMaintained, cloudAccounts,
- billingReference, planId);
- }
-
- @Override
- public LockedTenant with(LastLoginInfo lastLoginInfo) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores,
- archiveAccess, invalidateUserSessionsBefore, tenantRolesLastMaintained, cloudAccounts,
- billingReference, planId);
- }
-
- public Cloud withSecretStore(TenantSecretStore tenantSecretStore) {
- ArrayList<TenantSecretStore> secretStores = new ArrayList<>(tenantSecretStores);
- secretStores.add(tenantSecretStore);
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccess,
- invalidateUserSessionsBefore, tenantRolesLastMaintained, cloudAccounts,
- billingReference, planId);
- }
-
- public Cloud withoutSecretStore(TenantSecretStore tenantSecretStore) {
- ArrayList<TenantSecretStore> secretStores = new ArrayList<>(tenantSecretStores);
- secretStores.remove(tenantSecretStore);
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, secretStores, archiveAccess,
- invalidateUserSessionsBefore, tenantRolesLastMaintained, cloudAccounts,
- billingReference, planId);
- }
-
- public Cloud withArchiveAccess(ArchiveAccess archiveAccess) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess,
- invalidateUserSessionsBefore,tenantRolesLastMaintained, cloudAccounts,
- billingReference, planId);
- }
-
- public Cloud withInvalidateUserSessionsBefore(Instant invalidateUserSessionsBefore) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess,
- Optional.of(invalidateUserSessionsBefore), tenantRolesLastMaintained, cloudAccounts,
- billingReference, planId);
- }
-
- @Override
- public LockedTenant with(Instant tenantRolesLastMaintained) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess,
- invalidateUserSessionsBefore, tenantRolesLastMaintained, cloudAccounts,
- billingReference, planId);
- }
-
- @Override
- public LockedTenant withCloudAccounts(List<CloudAccountInfo> cloudAccounts) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess,
- invalidateUserSessionsBefore, tenantRolesLastMaintained, cloudAccounts,
- billingReference, planId);
- }
-
- public Cloud with(BillingReference billingReference) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess,
- invalidateUserSessionsBefore, tenantRolesLastMaintained, cloudAccounts,
- Optional.of(billingReference), planId);
- }
-
- public Cloud withPlanId(PlanId planId) {
- return new Cloud(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores, archiveAccess,
- invalidateUserSessionsBefore, tenantRolesLastMaintained, cloudAccounts,
- billingReference, planId);
- }
- }
-
-
- /** A locked DeletedTenant. */
- public static class Deleted extends LockedTenant {
-
- private final Instant deletedAt;
-
- private Deleted(DeletedTenant tenant) {
- super(tenant.name(), tenant.createdAt(), tenant.lastLoginInfo(), Instant.EPOCH, List.of());
- this.deletedAt = tenant.deletedAt();
- }
-
- @Override
- public DeletedTenant get() {
- return new DeletedTenant(name, createdAt, deletedAt);
- }
-
- @Override
- public LockedTenant with(LastLoginInfo lastLoginInfo) {
- return this;
- }
-
- @Override
- public LockedTenant with(Instant tenantRolesLastMaintained) {
- return this;
- }
-
- @Override
- public LockedTenant withCloudAccounts(List<CloudAccountInfo> cloudAccounts) {
- return this;
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/NotExistsException.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/NotExistsException.java
deleted file mode 100644
index 064a2a39860..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/NotExistsException.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Identifier;
-
-/**
- * An exception which indicates that a requested resource does not exist.
- *
- * @author Tony Vaagenes
- */
-public class NotExistsException extends IllegalArgumentException {
-
- public NotExistsException(String message) {
- super(message);
- }
-
- /**
- * Example message: Tenant 'myId' does not exist.
- *
- * @param capitalizedType e.g. Tenant, Application
- * @param id The id of the entity that didn't exist.
- *
- */
- public NotExistsException(String capitalizedType, String id) {
- super(Text.format("%s '%s' does not exist", capitalizedType, id));
- }
-
- public NotExistsException(Identifier id) {
- this(id.capitalizedType(), id.id());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/OsController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/OsController.java
deleted file mode 100644
index bec7c40d2a9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/OsController.java
+++ /dev/null
@@ -1,217 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.versions.CertifiedOsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
-
-import java.time.Instant;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.function.BinaryOperator;
-import java.util.function.Function;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * A singleton owned by {@link Controller} which contains the methods and state for controlling OS upgrades.
- *
- * @author mpolden
- */
-public record OsController(Controller controller) {
-
- private static final Logger LOG = Logger.getLogger(OsController.class.getName());
-
- public OsController {
- Objects.requireNonNull(controller);
- }
-
- /** Returns the target OS version for infrastructure in this system. The controller will drive infrastructure OS
- * upgrades to this version */
- public Optional<OsVersionTarget> target(CloudName cloud) {
- return targets().stream().filter(target -> target.osVersion().cloud().equals(cloud)).findFirst();
- }
-
- /** Returns all target OS versions in this system */
- public Set<OsVersionTarget> targets() {
- return curator().readOsVersionTargets();
- }
-
- /**
- * Set the target OS version for given cloud in this system.
- *
- * @param version The target OS version
- * @param cloud The cloud to upgrade
- * @param force Allow downgrades, and override pinned target (if any)
- * @param pin Pin this version. This prevents automatic scheduling of upgrades until version is unpinned
- */
- public void upgradeTo(Version version, CloudName cloud, boolean force, boolean pin) {
- requireNonEmpty(version);
- requireCloud(cloud);
- Instant scheduledAt = controller.clock().instant();
- try (Mutex lock = curator().lockOsVersions()) {
- Map<CloudName, OsVersionTarget> targets = curator().readOsVersionTargets().stream()
- .collect(Collectors.toMap(t -> t.osVersion().cloud(),
- Function.identity()));
-
- OsVersionTarget currentTarget = targets.get(cloud);
- boolean downgrade = false;
- if (currentTarget != null) {
- boolean versionChange = !currentTarget.osVersion().version().equals(version);
- downgrade = version.isBefore(currentTarget.osVersion().version());
- if (versionChange && currentTarget.pinned() && !force) {
- throw new IllegalArgumentException("Cannot " + (downgrade ? "downgrade" : "upgrade") + " cloud " +
- cloud.value() + "' to version " + version.toFullString() +
- ": Current target is pinned. Add 'force' parameter to override");
- }
- if (downgrade && !force) {
- throw new IllegalArgumentException("Cannot downgrade cloud '" + cloud.value() + "' to version " +
- version.toFullString() + ": Missing 'force' parameter");
- }
- if (!versionChange && currentTarget.pinned() == pin) return; // No change
- }
-
- OsVersionTarget newTarget = new OsVersionTarget(new OsVersion(version, cloud), scheduledAt, pin, downgrade);
- targets.put(cloud, newTarget);
- curator().writeOsVersionTargets(new TreeSet<>(targets.values()));
- LOG.info("Triggered OS " + (downgrade ? "downgrade" : "upgrade") + " to " + version.toFullString() +
- " in cloud " + cloud.value());
- }
- }
-
- /** Clear the target OS version for given cloud in this system */
- public void cancelUpgrade(CloudName cloudName) {
- try (Mutex lock = curator().lockOsVersions()) {
- Map<CloudName, OsVersionTarget> targets = curator().readOsVersionTargets().stream()
- .collect(Collectors.toMap(t -> t.osVersion().cloud(),
- Function.identity()));
- if (targets.remove(cloudName) == null) {
- throw new IllegalArgumentException("Cloud '" + cloudName.value() + " has no OS upgrade target");
- }
- curator().writeOsVersionTargets(new TreeSet<>(targets.values()));
- }
- }
-
- /** Returns the current OS version status */
- public OsVersionStatus status() {
- return curator().readOsVersionStatus();
- }
-
- /** Replace the current OS version status with a new one */
- public void updateStatus(OsVersionStatus newStatus) {
- try (Mutex lock = curator().lockOsVersionStatus()) {
- OsVersionStatus currentStatus = curator().readOsVersionStatus();
- for (CloudName cloud : controller.clouds()) {
- Set<Version> newVersions = newStatus.versionsIn(cloud);
- if (currentStatus.versionsIn(cloud).size() > 1 && newVersions.size() == 1) {
- LOG.info("All nodes in " + cloud + " cloud upgraded to OS version " +
- newVersions.iterator().next().toFullString());
- }
- }
- curator().writeOsVersionStatus(newStatus);
- }
- }
-
- /** Certify an OS version as compatible with given Vespa version */
- public CertifiedOsVersion certify(Version version, CloudName cloud, Version vespaVersion) {
- requireNonEmpty(version);
- requireNonEmpty(vespaVersion);
- requireCloud(cloud);
- try (Mutex lock = curator().lockCertifiedOsVersions()) {
- OsVersion osVersion = new OsVersion(version, cloud);
- Set<CertifiedOsVersion> certifiedVersions = readCertified();
- Optional<CertifiedOsVersion> matching = certifiedVersions.stream()
- .filter(cv -> cv.osVersion().equals(osVersion))
- .findFirst();
- if (matching.isPresent()) {
- return matching.get();
- }
- certifiedVersions = new HashSet<>(certifiedVersions);
- certifiedVersions.add(new CertifiedOsVersion(osVersion, vespaVersion));
- curator().writeCertifiedOsVersions(certifiedVersions);
- return new CertifiedOsVersion(osVersion, vespaVersion);
- }
- }
-
- /** Revoke certification of an OS version */
- public void uncertify(Version version, CloudName cloud) {
- try (Mutex lock = curator().lockCertifiedOsVersions()) {
- OsVersion osVersion = new OsVersion(version, cloud);
- Set<CertifiedOsVersion> certifiedVersions = readCertified();
- Optional<CertifiedOsVersion> existing = certifiedVersions.stream()
- .filter(cv -> cv.osVersion().equals(osVersion))
- .findFirst();
- if (existing.isEmpty()) {
- throw new IllegalArgumentException(osVersion + " is not certified");
- }
- certifiedVersions = new HashSet<>(certifiedVersions);
- certifiedVersions.remove(existing.get());
- curator().writeCertifiedOsVersions(certifiedVersions);
- }
- }
-
- /** Remove certifications for non-existent OS versions */
- public void removeStaleCertifications(OsVersionStatus currentStatus) {
- try (Mutex lock = curator().lockCertifiedOsVersions()) {
- Map<CloudName, Version> oldestVersionByCloud = currentStatus.versions().keySet().stream()
- .filter(v -> !v.version().isEmpty())
- .collect(Collectors.toMap(OsVersion::cloud,
- OsVersion::version,
- BinaryOperator.minBy(Comparator.naturalOrder())));
- if (oldestVersionByCloud.isEmpty()) return;
-
- Set<CertifiedOsVersion> certifiedVersions = new HashSet<>(readCertified());
- boolean modified = certifiedVersions.removeIf(certifiedVersion -> {
- Version oldestVersion = oldestVersionByCloud.get(certifiedVersion.osVersion().cloud());
- return oldestVersion == null || certifiedVersion.osVersion().version().isBefore(oldestVersion);
- });
- if (modified) {
- curator().writeCertifiedOsVersions(certifiedVersions);
- }
- }
- }
-
- /** Returns whether given OS version is certified as compatible with the current system version */
- public boolean certified(OsVersion osVersion) {
- if (controller.system().isCd()) return true; // Always certified (this is the system doing the certifying)
-
- Version systemVersion = controller.readSystemVersion();
- return readCertified().stream()
- .anyMatch(certifiedOsVersion -> certifiedOsVersion.osVersion().equals(osVersion) &&
- // A later system version is fine, as we don't guarantee that
- // an OS upgrade will always coincide with a Vespa release
- !certifiedOsVersion.vespaVersion().isAfter(systemVersion));
- }
-
- /** Returns all certified versions */
- public Set<CertifiedOsVersion> readCertified() {
- return controller.curator().readCertifiedOsVersions();
- }
-
- private void requireCloud(CloudName cloud) {
- if (!controller.clouds().contains(cloud)) {
- throw new IllegalArgumentException("Cloud '" + cloud + "' does not exist in this system");
- }
- }
-
- private void requireNonEmpty(Version version) {
- if (version.isEmpty()) {
- throw new IllegalArgumentException("Invalid version '" + version.toFullString() + "'");
- }
- }
-
- private CuratorDb curator() {
- return controller.curator();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
deleted file mode 100644
index 5dec1449507..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java
+++ /dev/null
@@ -1,624 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.google.common.hash.HashCode;
-import com.google.common.hash.Hashing;
-import com.google.common.io.BaseEncoding;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.StringFlag;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.Endpoint.Port;
-import com.yahoo.vespa.hosted.controller.application.Endpoint.Scope;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.EndpointList;
-import com.yahoo.vespa.hosted.controller.application.GeneratedEndpoint;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.BasicServicesXml;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue.Priority;
-import com.yahoo.vespa.hosted.controller.routing.EndpointConfig;
-import com.yahoo.vespa.hosted.controller.routing.GeneratedEndpointList;
-import com.yahoo.vespa.hosted.controller.routing.PreparedEndpoints;
-import com.yahoo.vespa.hosted.controller.routing.RoutingId;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicies;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicy;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicyList;
-import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext;
-import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext.ExclusiveDeploymentRoutingContext;
-import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext.SharedDeploymentRoutingContext;
-import com.yahoo.vespa.hosted.controller.routing.context.ExclusiveZoneRoutingContext;
-import com.yahoo.vespa.hosted.controller.routing.context.RoutingContext;
-import com.yahoo.vespa.hosted.controller.routing.context.SharedZoneRoutingContext;
-import com.yahoo.vespa.hosted.controller.routing.rotation.Rotation;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationLock;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationRepository;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static java.util.stream.Collectors.toMap;
-
-/**
- * The routing controller is owned by {@link Controller} and encapsulates state and methods for inspecting and
- * manipulating deployment endpoints in a hosted Vespa system.
- *
- * The one-stop shop for all your routing needs!
- *
- * @author mpolden
- */
-public class RoutingController {
-
- private static final Logger LOG = Logger.getLogger(RoutingController.class.getName());
-
- private final Controller controller;
- private final RoutingPolicies routingPolicies;
- private final RotationRepository rotationRepository;
- private final StringFlag endpointConfig;
-
- public RoutingController(Controller controller, RotationsConfig rotationsConfig) {
- this.controller = Objects.requireNonNull(controller, "controller must be non-null");
- this.routingPolicies = new RoutingPolicies(controller);
- this.rotationRepository = new RotationRepository(Objects.requireNonNull(rotationsConfig, "rotationsConfig must be non-null"),
- controller.applications(),
- controller.curator());
- this.endpointConfig = Flags.ENDPOINT_CONFIG.bindTo(controller.flagSource());
- }
-
- /** Create a routing context for given deployment */
- public DeploymentRoutingContext of(DeploymentId deployment) {
- if (usesSharedRouting(deployment.zoneId())) {
- return new SharedDeploymentRoutingContext(deployment,
- this,
- controller.serviceRegistry().configServer(),
- controller.clock());
- }
- return new ExclusiveDeploymentRoutingContext(deployment, this);
- }
-
- /** Create a routing context for given zone */
- public RoutingContext of(ZoneId zone) {
- if (usesSharedRouting(zone)) {
- return new SharedZoneRoutingContext(zone, controller.serviceRegistry().configServer());
- }
- return new ExclusiveZoneRoutingContext(zone, routingPolicies);
- }
-
- public RoutingPolicies policies() {
- return routingPolicies;
- }
-
- public RotationRepository rotations() {
- return rotationRepository;
- }
-
- /** Returns the endpoint config to use for given instance */
- public EndpointConfig endpointConfig(ApplicationId instance) {
- String flagValue = endpointConfig.with(FetchVector.Dimension.TENANT_ID, instance.tenant().value())
- .with(FetchVector.Dimension.APPLICATION, TenantAndApplicationId.from(instance).serialized())
- .with(FetchVector.Dimension.INSTANCE_ID, instance.serializedForm())
- .value();
- return switch (flagValue) {
- case "legacy" -> EndpointConfig.legacy;
- case "combined" -> EndpointConfig.combined;
- case "generated" -> EndpointConfig.generated;
- default -> throw new IllegalArgumentException("Invalid endpoint-config flag value: '" + flagValue + "', must be " +
- "'legacy', 'combined' or 'generated'");
- };
- }
-
- /** Prepares and returns the endpoints relevant for given deployment */
- public PreparedEndpoints prepare(DeploymentId deployment, BasicServicesXml services, EndpointCertificate certificate, LockedApplication application) {
- EndpointList endpoints = EndpointList.EMPTY;
- DeploymentSpec spec = application.get().deploymentSpec();
-
- // Assign rotations to application
- for (var instanceSpec : spec.instances()) {
- if (instanceSpec.concerns(Environment.prod)) {
- application = controller.routing().assignRotations(application, instanceSpec.name());
- }
- }
-
- // Add zone-scoped endpoints
- Map<EndpointId, List<GeneratedEndpoint>> generatedForDeclaredEndpoints = new HashMap<>();
- Set<ClusterSpec.Id> clustersWithToken = new HashSet<>();
- EndpointConfig config = endpointConfig(deployment.applicationId());
- RoutingPolicyList applicationPolicies = policies().read(TenantAndApplicationId.from(deployment.applicationId()));
- RoutingPolicyList deploymentPolicies = applicationPolicies.deployment(deployment);
- for (var container : services.containers()) {
- ClusterSpec.Id clusterId = ClusterSpec.Id.from(container.id());
- boolean tokenSupported = container.authMethods().contains(BasicServicesXml.Container.AuthMethod.token);
- if (tokenSupported) {
- clustersWithToken.add(clusterId);
- }
- Optional<RoutingPolicy> clusterPolicy = deploymentPolicies.cluster(clusterId).first();
- List<GeneratedEndpoint> generatedForCluster = clusterPolicy.map(policy -> policy.generatedEndpoints().cluster().asList())
- .orElseGet(List::of);
- // Generate endpoint for each auth method, if not present
- generatedForCluster = generateEndpoints(AuthMethod.mtls, certificate, Optional.empty(), generatedForCluster);
- if (tokenSupported) {
- generatedForCluster = generateEndpoints(AuthMethod.token, certificate, Optional.empty(), generatedForCluster);
- }
- GeneratedEndpointList generatedEndpoints = config.supportsGenerated() ? GeneratedEndpointList.copyOf(generatedForCluster) : GeneratedEndpointList.EMPTY;
- endpoints = endpoints.and(endpointsOf(deployment, clusterId, generatedEndpoints).scope(Scope.zone));
- }
-
- // Add global- and application-scoped endpoints
- for (var container : services.containers()) {
- ClusterSpec.Id clusterId = ClusterSpec.Id.from(container.id());
- applicationPolicies.cluster(clusterId).asList().stream()
- .flatMap(policy -> policy.generatedEndpoints().declared().asList().stream())
- .forEach(ge -> {
- List<GeneratedEndpoint> generated = generatedForDeclaredEndpoints.computeIfAbsent(ge.endpoint().get(), (k) -> new ArrayList<>());
- if (!generated.contains(ge)) {
- generated.add(ge);
- }
- });
- }
- // Generate endpoints if declared endpoint does not have any
- Stream.concat(spec.endpoints().stream(), spec.instances().stream().flatMap(i -> i.endpoints().stream()))
- .forEach(endpoint -> {
- EndpointId endpointId = EndpointId.of(endpoint.endpointId());
- generatedForDeclaredEndpoints.compute(endpointId, (k, old) -> {
- if (old == null) {
- old = List.of();
- }
- List<GeneratedEndpoint> generatedEndpoints = generateEndpoints(AuthMethod.mtls, certificate, Optional.of(endpointId), old);
- boolean tokenSupported = clustersWithToken.contains(ClusterSpec.Id.from(endpoint.containerId()));
- if (tokenSupported){
- generatedEndpoints = generateEndpoints(AuthMethod.token, certificate, Optional.of(endpointId), generatedEndpoints);
- }
- return generatedEndpoints;
- });
- });
- Map<EndpointId, GeneratedEndpointList> generatedEndpoints = config.supportsGenerated()
- ? generatedForDeclaredEndpoints.entrySet()
- .stream()
- .collect(Collectors.toMap(Map.Entry::getKey, kv -> GeneratedEndpointList.copyOf(kv.getValue())))
- : Map.of();
- endpoints = endpoints.and(declaredEndpointsOf(application.get().id(), spec, generatedEndpoints).targets(deployment));
- PreparedEndpoints prepared = new PreparedEndpoints(deployment,
- endpoints,
- application.get().require(deployment.applicationId().instance()).rotations(),
- certificate);
-
- // Register rotation-backed endpoints in DNS
- registerRotationEndpointsInDns(prepared);
-
- LOG.log(Level.FINE, () -> "Prepared endpoints: " + prepared);
-
- return prepared;
- }
-
- // -------------- Implicit endpoints (scopes 'zone' and 'weighted') --------------
-
- /** Returns the zone- and region-scoped endpoints of given deployment */
- public EndpointList endpointsOf(DeploymentId deployment, ClusterSpec.Id cluster, GeneratedEndpointList generatedEndpoints) {
- requireGeneratedEndpoints(generatedEndpoints, false);
- boolean generatedEndpointsAvailable = !generatedEndpoints.isEmpty();
- boolean tokenSupported = !generatedEndpoints.authMethod(AuthMethod.token).isEmpty();
- boolean isProduction = deployment.zoneId().environment().isProduction();
- RoutingMethod routingMethod = controller.zoneRegistry().routingMethod(deployment.zoneId());
- List<Endpoint> endpoints = new ArrayList<>();
- Endpoint.EndpointBuilder zoneEndpoint = Endpoint.of(deployment.applicationId())
- .routingMethod(routingMethod)
- .on(Port.fromRoutingMethod(routingMethod))
- .legacy(generatedEndpointsAvailable)
- .target(cluster, deployment);
- endpoints.add(zoneEndpoint.in(controller.system()));
- ZoneApi zone = controller.zoneRegistry().zones().all().get(deployment.zoneId()).get();
- Endpoint.EndpointBuilder regionEndpoint = Endpoint.of(deployment.applicationId())
- .routingMethod(routingMethod)
- .on(Port.fromRoutingMethod(routingMethod))
- .legacy(generatedEndpointsAvailable)
- .targetRegion(cluster,
- zone.getCloudNativeRegionName(),
- zone.getCloudName());
- // Region endpoints are only used by global- and application-endpoints and are thus only needed in
- // production environments
- if (isProduction) {
- endpoints.add(regionEndpoint.in(controller.system()));
- }
- for (var generatedEndpoint : generatedEndpoints) {
- boolean include = switch (generatedEndpoint.authMethod()) {
- case token -> tokenSupported;
- case mtls -> true;
- case none -> false;
- };
- if (include) {
- endpoints.add(zoneEndpoint.generatedFrom(generatedEndpoint)
- .legacy(false)
- .authMethod(generatedEndpoint.authMethod())
- .in(controller.system()));
- // Only a single region endpoint is needed, not one per auth method
- if (isProduction && generatedEndpoint.authMethod() == AuthMethod.mtls) {
- GeneratedEndpoint weightedGeneratedEndpoint = generatedEndpoint.withClusterPart(weightedClusterPart(cluster, deployment));
- endpoints.add(regionEndpoint.generatedFrom(weightedGeneratedEndpoint)
- .legacy(false)
- .authMethod(AuthMethod.none)
- .in(controller.system()));
- }
- }
- }
- return filterEndpoints(deployment.applicationId(), EndpointList.copyOf(endpoints));
- }
-
- /** Read routing policies and return zone- and region-scoped endpoints for given deployment */
- public EndpointList readEndpointsOf(DeploymentId deployment) {
- Set<Endpoint> endpoints = new LinkedHashSet<>();
- for (var policy : routingPolicies.read(deployment)) {
- endpoints.addAll(endpointsOf(deployment, policy.id().cluster(), policy.generatedEndpoints().cluster()).asList());
- }
- return EndpointList.copyOf(endpoints);
- }
-
- // -------------- Declared endpoints (scopes 'global' and 'application') --------------
-
- /** Returns global endpoints pointing to given deployments */
- public EndpointList declaredEndpointsOf(RoutingId routingId, ClusterSpec.Id cluster, List<DeploymentId> deployments, GeneratedEndpointList generatedEndpoints) {
- requireGeneratedEndpoints(generatedEndpoints, true);
- var endpoints = new ArrayList<Endpoint>();
- var directMethods = 0;
- var availableRoutingMethods = routingMethodsOfAll(deployments);
- boolean generatedEndpointsAvailable = !generatedEndpoints.isEmpty();
- for (var method : availableRoutingMethods) {
- if (method.isDirect() && ++directMethods > 1) {
- throw new IllegalArgumentException("Invalid routing methods for " + routingId + ": Exceeded maximum " +
- "direct methods");
- }
- Endpoint.EndpointBuilder builder = Endpoint.of(routingId.instance())
- .target(routingId.endpointId(), cluster, deployments)
- .on(Port.fromRoutingMethod(method))
- .legacy(generatedEndpointsAvailable)
- .routingMethod(method);
- endpoints.add(builder.in(controller.system()));
- for (var ge : generatedEndpoints) {
- endpoints.add(builder.generatedFrom(ge).legacy(false).authMethod(ge.authMethod()).in(controller.system()));
- }
- }
- return filterEndpoints(routingId.instance(), EndpointList.copyOf(endpoints));
- }
-
- /** Returns application endpoints pointing to given deployments */
- public EndpointList declaredEndpointsOf(TenantAndApplicationId application, EndpointId endpoint, ClusterSpec.Id cluster,
- Map<DeploymentId, Integer> deployments, GeneratedEndpointList generatedEndpoints) {
- requireGeneratedEndpoints(generatedEndpoints, true);
- ZoneId zone = deployments.keySet().iterator().next().zoneId(); // Where multiple zones are possible, they all have the same routing method.
- RoutingMethod routingMethod = usesSharedRouting(zone) ? RoutingMethod.sharedLayer4 : RoutingMethod.exclusive;
- boolean generatedEndpointsAvailable = !generatedEndpoints.isEmpty();
- Endpoint.EndpointBuilder builder = Endpoint.of(application)
- .targetApplication(endpoint,
- cluster,
- deployments)
- .routingMethod(routingMethod)
- .legacy(generatedEndpointsAvailable)
- .on(Port.fromRoutingMethod(routingMethod));
- List<Endpoint> endpoints = new ArrayList<>();
- endpoints.add(builder.in(controller.system()));
- for (var ge : generatedEndpoints) {
- endpoints.add(builder.generatedFrom(ge).legacy(false).authMethod(ge.authMethod()).in(controller.system()));
- }
- return EndpointList.copyOf(endpoints);
- }
-
- /** Read application and return endpoints for all instances in application */
- public EndpointList readDeclaredEndpointsOf(Application application) {
- return declaredEndpointsOf(application.id(), application.deploymentSpec(), readDeclaredGeneratedEndpoints(application.id()));
- }
-
- /** Read application and return declared endpoints for given instance */
- public EndpointList readDeclaredEndpointsOf(ApplicationId instance) {
- if (SystemApplication.matching(instance).isPresent()) return EndpointList.EMPTY;
- Application application = controller.applications().requireApplication(TenantAndApplicationId.from(instance));
- return readDeclaredEndpointsOf(application).instance(instance.instance());
- }
-
- private EndpointList declaredEndpointsOf(TenantAndApplicationId application, DeploymentSpec deploymentSpec, Map<EndpointId, GeneratedEndpointList> generatedEndpoints) {
- Set<Endpoint> endpoints = new LinkedHashSet<>();
- // Global endpoints
- for (var spec : deploymentSpec.instances()) {
- ApplicationId instance = application.instance(spec.name());
- for (var declaredEndpoint : spec.endpoints()) {
- RoutingId routingId = RoutingId.of(instance, EndpointId.of(declaredEndpoint.endpointId()));
- List<DeploymentId> deployments = declaredEndpoint.regions().stream()
- .map(region -> new DeploymentId(instance,
- ZoneId.from(Environment.prod, region)))
- .toList();
- ClusterSpec.Id cluster = ClusterSpec.Id.from(declaredEndpoint.containerId());
- GeneratedEndpointList generatedForId = generatedEndpoints.getOrDefault(routingId.endpointId(), GeneratedEndpointList.EMPTY);
- endpoints.addAll(declaredEndpointsOf(routingId, cluster, deployments, generatedForId).asList());
- }
- }
- // Application endpoints
- for (var declaredEndpoint : deploymentSpec.endpoints()) {
- Map<DeploymentId, Integer> deployments = declaredEndpoint.targets().stream()
- .collect(toMap(t -> new DeploymentId(application.instance(t.instance()),
- ZoneId.from(Environment.prod, t.region())),
- t -> t.weight()));
- ClusterSpec.Id cluster = ClusterSpec.Id.from(declaredEndpoint.containerId());
- EndpointId endpointId = EndpointId.of(declaredEndpoint.endpointId());
- GeneratedEndpointList generatedForId = generatedEndpoints.getOrDefault(endpointId, GeneratedEndpointList.EMPTY);
- endpoints.addAll(declaredEndpointsOf(application, endpointId, cluster, deployments, generatedForId).asList());
- }
- return EndpointList.copyOf(endpoints);
- }
-
- // -------------- Other gunk related to endpoints and routing --------------
-
- /** Read endpoints for use in deployment steps, for given deployments, grouped by their zone */
- public Map<ZoneId, List<Endpoint>> readStepRunnerEndpointsOf(Collection<DeploymentId> deployments) {
- TreeMap<ZoneId, List<Endpoint>> endpoints = new TreeMap<>(Comparator.comparing(ZoneId::value));
- for (var deployment : deployments) {
- EndpointList zoneEndpoints = readEndpointsOf(deployment).scope(Endpoint.Scope.zone)
- .authMethod(AuthMethod.mtls)
- .not().legacy();
- EndpointList directEndpoints = zoneEndpoints.direct();
- if (!directEndpoints.isEmpty()) {
- zoneEndpoints = directEndpoints; // Use only direct endpoints if we have any
- }
- EndpointList generatedEndpoints = zoneEndpoints.generated();
- if (!generatedEndpoints.isEmpty()) {
- zoneEndpoints = generatedEndpoints; // Use generated endpoints if we have any
- }
- if ( ! zoneEndpoints.isEmpty()) {
- endpoints.put(deployment.zoneId(), zoneEndpoints.asList());
- }
- }
- return Collections.unmodifiableSortedMap(endpoints);
- }
-
- /** Returns certificate DNS names (CN and SAN values) for given deployment */
- public List<String> certificateDnsNames(DeploymentId deployment, DeploymentSpec deploymentSpec, String generatedId, boolean legacy) {
- List<String> endpointDnsNames = new ArrayList<>();
- if (legacy) {
- endpointDnsNames.addAll(legacyCertificateDnsNames(deployment, deploymentSpec));
- }
- for (Scope scope : List.of(Scope.zone, Scope.global, Scope.application)) {
- endpointDnsNames.add(Endpoint.of(deployment.applicationId())
- .wildcardGenerated(generatedId, scope)
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .certificateName()
- .in(controller.system())
- .dnsName());
- }
- return Collections.unmodifiableList(endpointDnsNames);
- }
-
- private List<String> legacyCertificateDnsNames(DeploymentId deployment, DeploymentSpec deploymentSpec) {
- List<String> endpointDnsNames = new ArrayList<>();
-
- // We add first an endpoint name based on a hash of the application ID,
- // as the certificate provider requires the first CN to be < 64 characters long.
- endpointDnsNames.add(commonNameHashOf(deployment.applicationId(), controller.system()));
-
- List<Endpoint.EndpointBuilder> builders = new ArrayList<>();
- if (deployment.zoneId().environment().isProduction()) {
- // Add default and wildcard names for global endpoints
- builders.add(Endpoint.of(deployment.applicationId()).target(EndpointId.defaultId()));
- builders.add(Endpoint.of(deployment.applicationId()).wildcard());
-
- // Add default and wildcard names for each region targeted by application endpoints
- List<DeploymentId> deploymentTargets = deploymentSpec.endpoints().stream()
- .map(com.yahoo.config.application.api.Endpoint::targets)
- .flatMap(Collection::stream)
- .map(com.yahoo.config.application.api.Endpoint.Target::region)
- .distinct()
- .map(region -> new DeploymentId(deployment.applicationId(), ZoneId.from(Environment.prod, region)))
- .toList();
- TenantAndApplicationId application = TenantAndApplicationId.from(deployment.applicationId());
- for (var targetDeployment : deploymentTargets) {
- builders.add(Endpoint.of(application).targetApplication(EndpointId.defaultId(), targetDeployment));
- builders.add(Endpoint.of(application).wildcardApplication(targetDeployment));
- }
- }
-
- // Add default and wildcard names for zone endpoints
- builders.add(Endpoint.of(deployment.applicationId()).target(ClusterSpec.Id.from("default"), deployment));
- builders.add(Endpoint.of(deployment.applicationId()).wildcard(deployment));
-
- // Build all certificate names
- for (var builder : builders) {
- Endpoint endpoint = builder.certificateName()
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(controller.system());
- endpointDnsNames.add(endpoint.dnsName());
- }
- return Collections.unmodifiableList(endpointDnsNames);
- }
-
- /** Remove endpoints in DNS for all rotations assigned to given instance */
- public void removeRotationEndpointsFromDns(Application application, InstanceName instanceName) {
- Set<Endpoint> endpointsToRemove = new LinkedHashSet<>();
- Instance instance = application.require(instanceName);
- // Compute endpoints from rotations. When removing DNS records for rotation-based endpoints we cannot use the
- // deployment spec, because submitting an empty deployment spec is the first step of removing an application
- for (var rotation : instance.rotations()) {
- var deployments = rotation.regions().stream()
- .map(region -> new DeploymentId(instance.id(), ZoneId.from(Environment.prod, region)))
- .toList();
- GeneratedEndpointList generatedForId = readDeclaredGeneratedEndpoints(application.id()).getOrDefault(rotation.endpointId(), GeneratedEndpointList.EMPTY);
- endpointsToRemove.addAll(declaredEndpointsOf(RoutingId.of(instance.id(), rotation.endpointId()),
- rotation.clusterId(), deployments,
- generatedForId)
- .asList());
- }
- endpointsToRemove.forEach(endpoint -> controller.nameServiceForwarder()
- .removeRecords(Record.Type.CNAME,
- RecordName.from(endpoint.dnsName()),
- Priority.normal,
- Optional.of(application.id())));
- }
-
- private EndpointList filterEndpoints(ApplicationId instance, EndpointList endpoints) {
- return endpointConfig(instance) == EndpointConfig.generated ? endpoints.generated() : endpoints;
- }
-
- private void registerRotationEndpointsInDns(PreparedEndpoints prepared) {
- TenantAndApplicationId owner = TenantAndApplicationId.from(prepared.deployment().applicationId());
- EndpointList globalEndpoints = prepared.endpoints().scope(Scope.global);
- for (var assignedRotation : prepared.rotations()) {
- EndpointList rotationEndpoints = globalEndpoints.named(assignedRotation.endpointId(), Scope.global)
- .requiresRotation();
- // Skip rotations which do not apply to this zone
- if (!assignedRotation.regions().contains(prepared.deployment().zoneId().region())) {
- continue;
- }
- // Register names in DNS
- Rotation rotation = rotationRepository.requireRotation(assignedRotation.rotationId());
- for (var endpoint : rotationEndpoints) {
- controller.nameServiceForwarder().createRecord(
- new Record(Record.Type.CNAME, RecordName.from(endpoint.dnsName()), RecordData.fqdn(rotation.name())),
- Priority.normal,
- Optional.of(owner)
- );
- }
- }
- for (var endpoint : prepared.endpoints().scope(Scope.application).shared()) { // DNS for non-shared application endpoints is handled by RoutingPolicies
- Set<ZoneId> targetZones = endpoint.targets().stream()
- .map(t -> t.deployment().zoneId())
- .collect(Collectors.toUnmodifiableSet());
- if (targetZones.size() != 1) throw new IllegalArgumentException("Endpoint '" + endpoint.name() +
- "' must target a single zone, got " +
- targetZones);
- ZoneId targetZone = targetZones.iterator().next();
- String vipHostname = controller.zoneRegistry().getVipHostname(targetZone)
- .orElseThrow(() -> new IllegalArgumentException("No VIP configured for zone " + targetZone));
- controller.nameServiceForwarder().createRecord(
- new Record(Record.Type.CNAME, RecordName.from(endpoint.dnsName()), RecordData.fqdn(vipHostname)),
- Priority.normal,
- Optional.of(owner));
- }
- }
-
- /** Returns generated endpoints. A new endpoint is generated if no matching endpoint already exists */
- private List<GeneratedEndpoint> generateEndpoints(AuthMethod authMethod, EndpointCertificate certificate,
- Optional<EndpointId> declaredEndpoint,
- List<GeneratedEndpoint> current) {
- if (current.stream().anyMatch(e -> e.authMethod() == authMethod && e.endpoint().equals(declaredEndpoint))) {
- return current;
- }
- Optional<String> applicationPart = certificate.generatedId();
- if (applicationPart.isPresent()) {
- current = new ArrayList<>(current);
- current.add(new GeneratedEndpoint(GeneratedEndpoint.createPart(controller.random(true)),
- applicationPart.get(),
- authMethod,
- declaredEndpoint));
- }
- return current;
- }
-
- /** Generate the cluster part of a {@link GeneratedEndpoint} for use in a {@link Endpoint.Scope#weighted} endpoint */
- private String weightedClusterPart(ClusterSpec.Id cluster, DeploymentId deployment) {
- // This ID must be common for a given cluster in all deployments within the same cloud-native region
- String cloudNativeRegion = controller.zoneRegistry().zones().all().get(deployment.zoneId()).get().getCloudNativeRegionName();
- HashCode hash = Hashing.sha256().newHasher()
- .putString(cluster.value(), StandardCharsets.UTF_8)
- .putString(":", StandardCharsets.UTF_8)
- .putString(cloudNativeRegion, StandardCharsets.UTF_8)
- .putString(":", StandardCharsets.UTF_8)
- .putString(deployment.applicationId().serializedForm(), StandardCharsets.UTF_8)
- .hash();
- String alphabet = "abcdef";
- char letter = alphabet.charAt(Math.abs(hash.asInt()) % alphabet.length());
- return letter + hash.toString().substring(0, 7);
- }
-
- /** Returns existing generated endpoints, grouped by their {@link Scope#multiDeployment()} endpoint */
- private Map<EndpointId, GeneratedEndpointList> readDeclaredGeneratedEndpoints(TenantAndApplicationId application) {
- Map<EndpointId, GeneratedEndpointList> endpoints = new HashMap<>();
- for (var policy : policies().read(application)) {
- Map<EndpointId, GeneratedEndpointList> generatedForDeclared = policy.generatedEndpoints()
- .not().cluster()
- .groupingBy(ge -> ge.endpoint().get());
- generatedForDeclared.forEach(endpoints::putIfAbsent);
- }
- return endpoints;
- }
-
- /**
- * Assigns one or more global rotations to given application, if eligible. The given application is implicitly
- * stored, ensuring that the assigned rotation(s) are persisted when this returns.
- */
- private LockedApplication assignRotations(LockedApplication application, InstanceName instanceName) {
- try (RotationLock rotationLock = rotationRepository.lock()) {
- var rotations = rotationRepository.getOrAssignRotations(application.get().deploymentSpec(),
- application.get().require(instanceName),
- rotationLock);
- application = application.with(instanceName, instance -> instance.with(rotations));
- controller.applications().store(application); // store assigned rotation even if deployment fails
- }
- return application;
- }
-
- private boolean usesSharedRouting(ZoneId zone) {
- return controller.zoneRegistry().routingMethod(zone).isShared();
- }
-
- /** Returns the routing methods that are available across all given deployments */
- private List<RoutingMethod> routingMethodsOfAll(Collection<DeploymentId> deployments) {
- Map<RoutingMethod, Set<DeploymentId>> deploymentsByMethod = new HashMap<>();
- for (var deployment : deployments) {
- RoutingMethod routingMethod = controller.zoneRegistry().routingMethod(deployment.zoneId());
- deploymentsByMethod.computeIfAbsent(routingMethod, k -> new LinkedHashSet<>())
- .add(deployment);
- }
- List<RoutingMethod> routingMethods = new ArrayList<>();
- deploymentsByMethod.forEach((method, supportedDeployments) -> {
- if (supportedDeployments.containsAll(deployments)) {
- routingMethods.add(method);
- }
- });
- return Collections.unmodifiableList(routingMethods);
- }
-
- private static void requireGeneratedEndpoints(GeneratedEndpointList generatedEndpoints, boolean declared) {
- if (generatedEndpoints.asList().stream().anyMatch(ge -> ge.declared() != declared)) {
- throw new IllegalStateException("All generated endpoints require declared=" + declared +
- ", got " + generatedEndpoints);
- }
- }
-
- /** Create a common name based on a hash of given application. This must be less than 64 characters long. */
- private static String commonNameHashOf(ApplicationId application, SystemName system) {
- @SuppressWarnings("deprecation") // for Hashing.sha1()
- HashCode sha1 = Hashing.sha1().hashString(application.serializedForm(), StandardCharsets.UTF_8);
- String base32 = BaseEncoding.base32().omitPadding().lowerCase().encode(sha1.asBytes());
- return 'v' + base32 + Endpoint.internalDnsSuffix(system);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
deleted file mode 100644
index 55269e2612f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/TenantController.java
+++ /dev/null
@@ -1,228 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.text.Text;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.concurrent.Once;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.security.AccessControl;
-import com.yahoo.vespa.hosted.controller.security.Credentials;
-import com.yahoo.vespa.hosted.controller.security.TenantSpec;
-import com.yahoo.vespa.hosted.controller.tenant.CloudAccountInfo;
-import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.function.Consumer;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * A singleton owned by the {@link Controller} which contains the methods and state for controlling tenants.
- *
- * @author bratseth
- * @author mpolden
- */
-public class TenantController {
-
- private static final Logger log = Logger.getLogger(TenantController.class.getName());
-
- private final Controller controller;
- private final CuratorDb curator;
- private final AccessControl accessControl;
-
- public TenantController(Controller controller, CuratorDb curator, AccessControl accessControl) {
- this.controller = Objects.requireNonNull(controller, "controller must be non-null");
- this.curator = Objects.requireNonNull(curator, "curator must be non-null");
- this.accessControl = Objects.requireNonNull(accessControl, "accessControl must be non-null");
-
- // Update serialization format of all tenants
- Once.after(Duration.ofMinutes(1), () -> {
- Instant start = controller.clock().instant();
- int count = 0;
- for (TenantName name : curator.readTenantNames()) {
- lockIfPresent(name, LockedTenant.class, this::store);
- count++;
- }
- log.log(Level.INFO, Text.format("Wrote %d tenants in %s", count,
- Duration.between(start, controller.clock().instant())));
- });
- }
-
- /** Returns a list of all known, non-deleted tenants sorted by name */
- public List<Tenant> asList() {
- return asList(false);
- }
-
- /** Returns a list of all known tenants sorted by name */
- public List<Tenant> asList(boolean includeDeleted) {
- return curator.readTenants().stream()
- .filter(tenant -> tenant.type() != Tenant.Type.deleted || includeDeleted)
- .sorted(Comparator.comparing(Tenant::name))
- .toList();
- }
-
- /** Locks a tenant for modification and applies the given action. */
- public <T extends LockedTenant> void lockIfPresent(TenantName name, Class<T> token, Consumer<T> action) {
- try (Mutex lock = lock(name)) {
- get(name).map(tenant -> LockedTenant.of(tenant, lock))
- .map(token::cast)
- .ifPresent(action);
- }
- }
-
- /** Lock a tenant for modification and apply action. Throws if the tenant does not exist */
- public <T extends LockedTenant> void lockOrThrow(TenantName name, Class<T> token, Consumer<T> action) {
- try (Mutex lock = lock(name)) {
- action.accept(token.cast(LockedTenant.of(require(name), lock)));
- }
- }
-
- /** Returns the tenant with the given name, or throws. */
- public Tenant require(TenantName name) {
- return get(name).orElseThrow(() -> new IllegalArgumentException("No such tenant '" + name + "'."));
- }
-
- /** Returns the tenant with the given name, and ensures the type */
- public <T extends Tenant> T require(TenantName name, Class<T> tenantType) {
- return get(name)
- .map(t -> {
- try { return tenantType.cast(t); } catch (ClassCastException e) {
- throw new IllegalArgumentException("Tenant '" + name + "' was of type '" + t.getClass().getSimpleName() + "' and not '" + tenantType.getSimpleName() + "'");
- }
- })
- .orElseThrow(() -> new IllegalArgumentException("No such tenant '" + name + "'."));
- }
-
- /** Replace and store any previous version of given tenant */
- public void store(LockedTenant tenant) {
- curator.writeTenant(tenant.get());
- }
-
- /** Create a tenant, provided the given credentials are valid. */
- public void create(TenantSpec tenantSpec, Credentials credentials) {
- try (Mutex lock = lock(tenantSpec.tenant())) {
- TenantId.validate(tenantSpec.tenant().value());
- requireNonExistent(tenantSpec.tenant());
- curator.writeTenant(accessControl.createTenant(tenantSpec, controller.clock().instant(), credentials, asList()));
-
- // We should create tenant roles here but it takes too long - assuming the TenantRoleMaintainer will do it Soon™
- }
- }
-
- /** Find tenant by name */
- public Optional<Tenant> get(TenantName name) {
- return get(name, false);
- }
-
- public Optional<Tenant> get(TenantName name, boolean includeDeleted) {
- return curator.readTenant(name)
- .filter(tenant -> tenant.type() != Tenant.Type.deleted || includeDeleted);
- }
-
- /** Find tenant by name */
- public Optional<Tenant> get(String name) {
- return get(TenantName.from(name));
- }
-
- /** Updates the tenant contained in the given tenant spec with new data. */
- public void update(TenantSpec tenantSpec, Credentials credentials) {
- try (Mutex lock = lock(tenantSpec.tenant())) {
- curator.writeTenant(accessControl.updateTenant(tenantSpec, credentials, asList(),
- controller.applications().asList(tenantSpec.tenant())));
- }
- }
-
- /**
- * Update last login times for the given tenant at the given user levers with the given instant, but only if the
- * new instant is later
- */
- public void updateLastLogin(TenantName tenantName, List<LastLoginInfo.UserLevel> userLevels, Instant loggedInAt) {
- try (Mutex lock = lock(tenantName)) {
- Tenant tenant = require(tenantName);
- LastLoginInfo loginInfo = tenant.lastLoginInfo();
- for (LastLoginInfo.UserLevel userLevel : userLevels)
- loginInfo = loginInfo.withLastLoginIfLater(userLevel, loggedInAt);
-
- if (tenant.lastLoginInfo().equals(loginInfo)) return; // no change
- curator.writeTenant(LockedTenant.of(tenant, lock).with(loginInfo).get());
- }
- }
-
- public void updateLastTenantRolesMaintained(TenantName tenantName, Instant lastMaintained) {
- try (Mutex lock = lock(tenantName)) {
- var tenant = require(tenantName);
- curator.writeTenant(LockedTenant.of(tenant, lock).with(lastMaintained).get());
- }
- }
-
- public void updateCloudAccounts(TenantName tenantName, List<CloudAccountInfo> cloudAccounts) {
- try (Mutex lock = lock(tenantName)) {
- var tenant = require(tenantName);
- if (tenant.cloudAccounts().equals(cloudAccounts)) return; // no change
- curator.writeTenant(LockedTenant.of(tenant, lock).withCloudAccounts(cloudAccounts).get());
- }
- }
-
- /** Deletes the given tenant. */
- public void delete(TenantName tenant, Optional<Credentials> credentials, boolean forget) {
- try (Mutex lock = lock(tenant)) {
- Tenant oldTenant = get(tenant, true)
- .orElseThrow(() -> new NotExistsException("Could not delete tenant '" + tenant + "': Tenant not found"));
-
- if (oldTenant.type() != Tenant.Type.deleted) {
- if (!controller.applications().asList(tenant).isEmpty())
- throw new IllegalArgumentException("Could not delete tenant '" + tenant.value()
- + "': This tenant has active applications");
-
- if (oldTenant.type() == Tenant.Type.athenz) {
- credentials.ifPresent(creds -> accessControl.deleteTenant(tenant, creds));
- } else if (oldTenant.type() == Tenant.Type.cloud) {
- accessControl.deleteTenant(tenant, null);
- } else {
- throw new IllegalArgumentException("Could not delete tenant '" + tenant.value()
- + ": This tenant is of unhandled type " + oldTenant.type());
- }
-
- controller.notificationsDb().removeNotifications(NotificationSource.from(tenant));
- }
-
- if (forget) curator.removeTenant(tenant);
- else curator.writeTenant(new DeletedTenant(tenant, oldTenant.createdAt(), controller.clock().instant()));
- }
- }
-
- private void requireNonExistent(TenantName name) {
- var tenant = get(name, true);
- if (tenant.isPresent() && tenant.get().type().equals(Tenant.Type.deleted)) {
- throw new IllegalArgumentException("Tenant '" + name + "' cannot be created, try a different name");
- }
- if (SystemApplication.TENANT.equals(name)
- || tenant.isPresent()
- // Underscores are allowed in existing tenant names, but tenants with - and _ cannot co-exist. E.g.
- // my-tenant cannot be created if my_tenant exists.
- || get(name.value().replace('-', '_')).isPresent()) {
- throw new IllegalArgumentException("Tenant '" + name + "' already exists");
- }
- }
-
- /**
- * Returns a lock which provides exclusive rights to changing this tenant.
- * Any operation which stores a tenant need to first acquire this lock, then read, modify
- * and store the tenant, and finally release (close) the lock.
- */
- private Mutex lock(TenantName tenant) {
- return curator.lock(tenant);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationActivity.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationActivity.java
deleted file mode 100644
index d89f786714d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationActivity.java
+++ /dev/null
@@ -1,78 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import java.time.Instant;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalDouble;
-import java.util.function.Function;
-
-/**
- * Recent activity in an application.
- *
- * @author mpolden
- */
-public class ApplicationActivity {
-
- public static final ApplicationActivity none = new ApplicationActivity(Optional.empty(), Optional.empty(),
- OptionalDouble.empty(),
- OptionalDouble.empty());
-
- private final Optional<Instant> lastQueried;
- private final Optional<Instant> lastWritten;
- private final OptionalDouble lastQueriesPerSecond;
- private final OptionalDouble lastWritesPerSecond;
-
- private ApplicationActivity(Optional<Instant> lastQueried, Optional<Instant> lastWritten,
- OptionalDouble lastQueriesPerSecond, OptionalDouble lastWritesPerSecond) {
- this.lastQueried = Objects.requireNonNull(lastQueried, "lastQueried must be non-null");
- this.lastWritten = Objects.requireNonNull(lastWritten, "lastWritten must be non-null");
- this.lastQueriesPerSecond = Objects.requireNonNull(lastQueriesPerSecond, "lastQueriesPerSecond must be non-null");
- this.lastWritesPerSecond = Objects.requireNonNull(lastWritesPerSecond, "lastWritesPerSecond must be non-null");
- }
-
- /** The last time any deployment in this was queried */
- public Optional<Instant> lastQueried() {
- return lastQueried;
- }
-
- /** The last time any deployment in this was written */
- public Optional<Instant> lastWritten() {
- return lastWritten;
- }
-
- /** Query rate the last time this was queried */
- public OptionalDouble lastQueriesPerSecond() {
- return lastQueriesPerSecond;
- }
-
- /** Write rate the last time this was written */
- public OptionalDouble lastWritesPerSecond() {
- return lastWritesPerSecond;
- }
-
- public static ApplicationActivity from(Collection<Deployment> deployments) {
- Optional<DeploymentActivity> lastActivityByQuery = lastActivityBy(DeploymentActivity::lastQueried, deployments);
- Optional<DeploymentActivity> lastActivityByWrite = lastActivityBy(DeploymentActivity::lastWritten, deployments);
- if (lastActivityByQuery.isEmpty() && lastActivityByWrite.isEmpty()) {
- return none;
- }
- return new ApplicationActivity(lastActivityByQuery.flatMap(DeploymentActivity::lastQueried),
- lastActivityByWrite.flatMap(DeploymentActivity::lastWritten),
- lastActivityByQuery.map(DeploymentActivity::lastQueriesPerSecond)
- .orElseGet(OptionalDouble::empty),
- lastActivityByWrite.map(DeploymentActivity::lastWritesPerSecond)
- .orElseGet(OptionalDouble::empty));
- }
-
- private static Optional<DeploymentActivity> lastActivityBy(Function<DeploymentActivity, Optional<Instant>> field,
- Collection<Deployment> deployments) {
- return deployments.stream()
- .map(Deployment::activity)
- .filter(activity -> field.apply(activity).isPresent())
- .max(Comparator.comparing(activity -> field.apply(activity).get()));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java
deleted file mode 100644
index 32aae5c041c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/ApplicationList.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.collections.AbstractFilteringList;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-
-import java.util.Collection;
-
-/**
- * A list of applications which can be filtered in various ways.
- *
- * @author jonmv
- */
-public class ApplicationList extends AbstractFilteringList<Application, ApplicationList> {
-
- private ApplicationList(Collection<? extends Application> applications, boolean negate) {
- super(applications, negate, ApplicationList::new);
- }
-
- // ----------------------------------- Factories
-
- public static ApplicationList from(Collection<? extends Application> applications) {
- return new ApplicationList(applications, false);
- }
-
- public static ApplicationList from(Collection<ApplicationId> ids, ApplicationController applications) {
- return from(ids.stream()
- .map(TenantAndApplicationId::from)
- .distinct()
- .map(applications::requireApplication)
- .toList());
- }
-
- // ----------------------------------- Filters
-
- /** Returns the subset of applications which have at least one production deployment */
- public ApplicationList withProductionDeployment() {
- return matching(application -> application.instances().values().stream()
- .anyMatch(instance -> instance.productionDeployments().size() > 0));
- }
-
- /** Returns the subset of applications with at least one declared job in deployment spec. */
- public ApplicationList withJobs() {
- return matching(application -> application.deploymentSpec().steps().stream()
- .anyMatch(step -> ! step.zones().isEmpty()));
- }
-
- /** Returns the subset of applications which have a project ID */
- public ApplicationList withProjectId() {
- return matching(application -> application.projectId().isPresent());
- }
-
- /** Returns the subset of application which have submitted a non-empty deployment spec. */
- public ApplicationList withDeploymentSpec() {
- return matching(application -> ! DeploymentSpec.empty.equals(application.deploymentSpec()));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java
deleted file mode 100644
index c2949e395e9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/AssignedRotation.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationId;
-
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * Contains the tuple of [clusterId, endpointId, rotationId, regions[]], to keep track
- * of which services have assigned which rotations under which name.
- *
- * @author ogronnesby
- */
-public record AssignedRotation(ClusterSpec.Id clusterId, EndpointId endpointId, RotationId rotationId, Set<RegionName> regions) {
-
- public AssignedRotation(ClusterSpec.Id clusterId, EndpointId endpointId, RotationId rotationId, Set<RegionName> regions) {
- this.clusterId = requireNonEmpty(clusterId, clusterId.value(), "clusterId");
- this.endpointId = Objects.requireNonNull(endpointId);
- this.rotationId = Objects.requireNonNull(rotationId);
- this.regions = Set.copyOf(Objects.requireNonNull(regions));
- }
-
- private static <T> T requireNonEmpty(T object, String value, String field) {
- Objects.requireNonNull(object);
- Objects.requireNonNull(value);
- if (value.isEmpty()) {
- throw new IllegalArgumentException("Field '" + field + "' was empty");
- }
- return object;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java
deleted file mode 100644
index b41b02011b4..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Change.java
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.component.Version;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.StringJoiner;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * The changes to an application we currently wish to complete deploying.
- * A goal of the system is to deploy platform and application versions separately.
- * However, this goal must some times be traded against others, so a change can
- * consist of both an application and platform version change.
- *
- * This is immutable.
- *
- * @author bratseth
- */
-public final class Change {
-
- private static final Change empty = new Change(Optional.empty(), Optional.empty(), false, false);
-
- /** The platform version we are upgrading to, or empty if none */
- private final Optional<Version> platform;
-
- /** The application version we are changing to, or empty if none */
- private final Optional<RevisionId> revision;
-
- /** Whether this change is a pin to its contained Vespa version, or to the application's current. */
- private final boolean platformPinned;
-
- /** Whether this change is a pin to its contained application revision, or to the application's current. */
- private final boolean revisionPinned;
-
- private Change(Optional<Version> platform, Optional<RevisionId> revision, boolean platformPinned, boolean revisionPinned) {
- this.platform = requireNonNull(platform, "platform cannot be null");
- this.revision = requireNonNull(revision, "revision cannot be null");
- if (revision.isPresent() && ( ! revision.get().isProduction())) {
- throw new IllegalArgumentException("Application version to deploy must be a known version");
- }
- this.platformPinned = platformPinned;
- this.revisionPinned = revisionPinned;
- }
-
- public Change withoutPlatform() {
- return new Change(Optional.empty(), revision, platformPinned, revisionPinned);
- }
-
- public Change withoutApplication() {
- return new Change(platform, Optional.empty(), platformPinned, revisionPinned);
- }
-
- /** Returns whether a change should currently be deployed */
- public boolean hasTargets() {
- return platform.isPresent() || revision.isPresent();
- }
-
- /** Returns whether this is the empty change. */
- public boolean isEmpty() {
- return ! hasTargets() && ! platformPinned && ! revisionPinned;
- }
-
- /** Returns the platform version carried by this. */
- public Optional<Version> platform() { return platform; }
-
- /** Returns the application version carried by this. */
- public Optional<RevisionId> revision() { return revision; }
-
- public boolean isPlatformPinned() { return platformPinned; }
-
- public boolean isRevisionPinned() { return revisionPinned; }
-
- /** Returns an instance representing no change */
- public static Change empty() { return empty; }
-
- /** Returns a version of this change which replaces or adds this platform change */
- public Change with(Version platformVersion) {
- if (platformPinned)
- throw new IllegalArgumentException("Not allowed to set a platform version when pinned.");
-
- return new Change(Optional.of(platformVersion), revision, platformPinned, revisionPinned);
- }
-
- /** Returns a version of this change which replaces or adds this revision change */
- public Change with(RevisionId revision) {
- if (revisionPinned)
- throw new IllegalArgumentException("Not allowed to set a revision when pinned.");
-
- return new Change(platform, Optional.of(revision), platformPinned, revisionPinned);
- }
-
- /** Returns a change with the versions of this, and with the platform version pinned. */
- public Change withPlatformPin() {
- return new Change(platform, revision, true, revisionPinned);
- }
-
- /** Returns a change with the versions of this, and with the platform version unpinned. */
- public Change withoutPlatformPin() {
- return new Change(platform, revision, false, revisionPinned);
- }
-
- /** Returns a change with the versions of this, and with the platform version pinned. */
- public Change withRevisionPin() {
- return new Change(platform, revision, platformPinned, true);
- }
-
- /** Returns a change with the versions of this, and with the platform version unpinned. */
- public Change withoutRevisionPin() {
- return new Change(platform, revision, platformPinned, false);
- }
-
- /** Returns the change obtained when overwriting elements of the given change with any present in this */
- public Change onTopOf(Change other) {
- if (platform.isPresent()) other = other.with(platform.get());
- if (revision.isPresent()) other = other.with(revision.get());
- if (platformPinned) other = other.withPlatformPin();
- if (revisionPinned) other = other.withRevisionPin();
- return other;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (!(o instanceof Change)) return false;
- Change change = (Change) o;
- return platformPinned == change.platformPinned &&
- revisionPinned == change.revisionPinned &&
- Objects.equals(platform, change.platform) &&
- Objects.equals(revision, change.revision);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(platform, revision, platformPinned, revisionPinned);
- }
-
- @Override
- public String toString() {
- StringJoiner changes = new StringJoiner(" and ");
- if (platformPinned)
- changes.add("pin to " + platform.map(Version::toString).orElse("current platform"));
- else
- platform.ifPresent(version -> changes.add("upgrade to " + version));
- if (revisionPinned)
- changes.add("pin to " + revision.map(RevisionId::toString).orElse("current revision"));
- else
- revision.ifPresent(revision -> changes.add("revision change to " + revision));
- changes.setEmptyValue("no change");
- return changes.toString();
- }
-
- public static Change of(RevisionId revision) {
- return new Change(Optional.empty(), Optional.of(revision), false, false);
- }
-
- public static Change of(Version platformChange) {
- return new Change(Optional.of(platformChange), Optional.empty(), false, false);
- }
-
- /** Returns whether this change carries a revision downgrade relative to the given revision. */
- public boolean downgrades(RevisionId revision) {
- return this.revision.map(revision::compareTo).orElse(0) > 0;
- }
-
- /** Returns whether this change carries a platform downgrade relative to the given version. */
- public boolean downgrades(Version version) {
- return platform.map(version::compareTo).orElse(0) > 0;
- }
-
- /** Returns whether this change carries a revision upgrade relative to the given revision. */
- public boolean upgrades(RevisionId revision) {
- return this.revision.map(revision::compareTo).orElse(0) < 0;
- }
-
- /** Returns whether this change carries a platform upgrade relative to the given version. */
- public boolean upgrades(Version version) {
- return platform.map(version::compareTo).orElse(0) < 0;
- }
-
-}
-
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java
deleted file mode 100644
index de26ca73cd8..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Deployment.java
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalDouble;
-
-/**
- * A deployment of an application in a particular zone.
- *
- * @author bratseth
- * @author smorgrav
- */
-public class Deployment {
-
- private final ZoneId zone;
- private final CloudAccount cloudAccount;
- private final RevisionId revision;
- private final Version version;
- private final Instant deployTime;
- private final DeploymentMetrics metrics;
- private final DeploymentActivity activity;
- private final QuotaUsage quota;
- private final OptionalDouble cost;
- private final Map<TokenId, Instant> dataPlaneTokens;
-
- public Deployment(ZoneId zone, CloudAccount cloudAccount, RevisionId revision, Version version, Instant deployTime,
- DeploymentMetrics metrics, DeploymentActivity activity, QuotaUsage quota, OptionalDouble cost,
- Map<TokenId, Instant> dataPlaneTokens) {
- this.zone = Objects.requireNonNull(zone, "zone cannot be null");
- this.cloudAccount = Objects.requireNonNull(cloudAccount, "cloudAccount cannot be null");
- this.revision = Objects.requireNonNull(revision, "revision cannot be null");
- this.version = Objects.requireNonNull(version, "version cannot be null");
- this.deployTime = Objects.requireNonNull(deployTime, "deployTime cannot be null");
- this.metrics = Objects.requireNonNull(metrics, "deploymentMetrics cannot be null");
- this.activity = Objects.requireNonNull(activity, "activity cannot be null");
- this.quota = Objects.requireNonNull(quota, "usage cannot be null");
- this.cost = Objects.requireNonNull(cost, "cost cannot be null");
- this.dataPlaneTokens = Map.copyOf(dataPlaneTokens);
- }
-
- /** Returns the zone this was deployed to */
- public ZoneId zone() { return zone; }
-
- /** Returns the cloud account this was deployed to */
- public CloudAccount cloudAccount() { return cloudAccount; }
-
- /** Returns the deployed application revision */
- public RevisionId revision() { return revision; }
-
- /** Returns the deployed Vespa version */
- public Version version() { return version; }
-
- /** Returns the time this was deployed */
- public Instant at() { return deployTime; }
-
- /** Returns metrics for this */
- public DeploymentMetrics metrics() {
- return metrics;
- }
-
- /** Returns activity for this */
- public DeploymentActivity activity() { return activity; }
-
- /** Returns quota usage for this */
- public QuotaUsage quota() { return quota; }
-
- /** Returns cost, in dollars per hour, for this */
- public OptionalDouble cost() { return cost; }
-
- /** Returns the data plane token IDs referenced by this deployment, and the last update time of this token at the time of deployment. */
- public Map<TokenId, Instant> dataPlaneTokens() { return dataPlaneTokens; }
-
- public Deployment recordActivityAt(Instant instant) {
- return new Deployment(zone, cloudAccount, revision, version, deployTime, metrics,
- activity.recordAt(instant, metrics), quota, cost, dataPlaneTokens);
- }
-
- public Deployment withMetrics(DeploymentMetrics metrics) {
- return new Deployment(zone, cloudAccount, revision, version, deployTime, metrics, activity, quota, cost, dataPlaneTokens);
- }
-
- public Deployment withCost(double cost) {
- if (this.cost.isPresent() && Double.compare(this.cost.getAsDouble(), cost) == 0) return this;
- return new Deployment(zone, cloudAccount, revision, version, deployTime, metrics, activity, quota, OptionalDouble.of(cost), dataPlaneTokens);
- }
-
- public Deployment withoutCost() {
- if (cost.isEmpty()) return this;
- return new Deployment(zone, cloudAccount, revision, version, deployTime, metrics, activity, quota, OptionalDouble.empty(), dataPlaneTokens);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Deployment that = (Deployment) o;
- return Objects.equals(zone, that.zone)
- && Objects.equals(cloudAccount, that.cloudAccount)
- && Objects.equals(revision, that.revision)
- && Objects.equals(version, that.version)
- && Objects.equals(deployTime, that.deployTime)
- && Objects.equals(metrics, that.metrics)
- && Objects.equals(activity, that.activity)
- && Objects.equals(quota, that.quota)
- && Objects.equals(cost, that.cost)
- && Objects.equals(dataPlaneTokens, that.dataPlaneTokens);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(zone, cloudAccount, revision, version, deployTime, metrics, activity, quota, cost, dataPlaneTokens);
- }
-
- @Override
- public String toString() {
- return "deployment to " + zone + " of " + revision + " on version " + version + " at " + deployTime;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentActivity.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentActivity.java
deleted file mode 100644
index d671f57f90f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentActivity.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import java.time.Instant;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalDouble;
-
-/**
- * Recent activity in a deployment.
- *
- * @author mpolden
- */
-public class DeploymentActivity {
-
- /** Query rates at or below this threshold indicate inactivity */
- private static final double inactivityThreshold = 0;
-
- public static final DeploymentActivity none = new DeploymentActivity(Optional.empty(), Optional.empty(),
- OptionalDouble.empty(),
- OptionalDouble.empty());
-
- private final Optional<Instant> lastQueried;
- private final Optional<Instant> lastWritten;
- private final OptionalDouble lastQueriesPerSecond;
- private final OptionalDouble lastWritesPerSecond;
-
- private DeploymentActivity(Optional<Instant> lastQueried, Optional<Instant> lastWritten,
- OptionalDouble lastQueriesPerSecond, OptionalDouble lastWritesPerSecond) {
- this.lastQueried = Objects.requireNonNull(lastQueried, "lastQueried must be non-null");
- this.lastWritten = Objects.requireNonNull(lastWritten, "lastWritten must be non-null");
- this.lastQueriesPerSecond = Objects.requireNonNull(lastQueriesPerSecond, "lastQueriesPerSecond must be non-null");
- this.lastWritesPerSecond = Objects.requireNonNull(lastWritesPerSecond, "lastWritesPerSecond must be non-null");
- }
-
- /** The last time this deployment received queries (search) */
- public Optional<Instant> lastQueried() {
- return lastQueried;
- }
-
- /** The last time this deployment received writes (feed) */
- public Optional<Instant> lastWritten() {
- return lastWritten;
- }
-
- /** Query rate the last time this deployment received queries (search) */
- public OptionalDouble lastQueriesPerSecond() {
- return lastQueriesPerSecond;
- }
-
- /** Write rate the last time this deployment received writes (feed) */
- public OptionalDouble lastWritesPerSecond() {
- return lastWritesPerSecond;
- }
-
- /** Record activity using given metrics */
- public DeploymentActivity recordAt(Instant instant, DeploymentMetrics metrics) {
- return new DeploymentActivity(activityAt(instant, lastQueried, metrics.queriesPerSecond()),
- activityAt(instant, lastWritten, metrics.writesPerSecond()),
- activeRate(metrics.queriesPerSecond(), lastQueriesPerSecond),
- activeRate(metrics.writesPerSecond(), lastWritesPerSecond));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- DeploymentActivity that = (DeploymentActivity) o;
- return lastQueried.equals(that.lastQueried) && lastWritten.equals(that.lastWritten) && lastQueriesPerSecond.equals(that.lastQueriesPerSecond) && lastWritesPerSecond.equals(that.lastWritesPerSecond);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(lastQueried, lastWritten, lastQueriesPerSecond, lastWritesPerSecond);
- }
-
- public static DeploymentActivity create(Optional<Instant> queriedAt, Optional<Instant> writtenAt,
- OptionalDouble lastQueriesPerSecond, OptionalDouble lastWritesPerSecond) {
- if (queriedAt.isEmpty() && writtenAt.isEmpty()) {
- return none;
- }
- return new DeploymentActivity(queriedAt, writtenAt, lastQueriesPerSecond, lastWritesPerSecond);
- }
-
- public static DeploymentActivity create(Optional<Instant> queriedAt, Optional<Instant> writtenAt) {
- return create(queriedAt, writtenAt, OptionalDouble.empty(), OptionalDouble.empty());
- }
-
- private static OptionalDouble activeRate(double newRate, OptionalDouble oldRate) {
- return newRate > inactivityThreshold ? OptionalDouble.of(newRate) : oldRate;
- }
-
- private static Optional<Instant> activityAt(Instant newInstant, Optional<Instant> oldInstant, double rate) {
- return rate > inactivityThreshold ? Optional.of(newInstant) : oldInstant;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java
deleted file mode 100644
index ce652521a9f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentMetrics.java
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import java.time.Instant;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Metrics for a deployment of an application. This contains a snapshot of metrics gathered at a point in time, it does
- * not contain any historical data.
- *
- * @author smorgrav
- * @author mpolden
- */
-public class DeploymentMetrics {
-
- public static final DeploymentMetrics none = new DeploymentMetrics(0, 0, 0, 0, 0, Optional.empty(), Map.of());
-
- private final double queriesPerSecond;
- private final double writesPerSecond;
- private final double documentCount;
- private final double queryLatencyMillis;
- private final double writeLatencyMills;
- private final Optional<Instant> instant;
- private final Map<Warning, Integer> warnings;
-
- /* DO NOT USE. Public for serialization purposes */
- public DeploymentMetrics(double queriesPerSecond, double writesPerSecond, double documentCount,
- double queryLatencyMillis, double writeLatencyMills, Optional<Instant> instant,
- Map<Warning, Integer> warnings) {
- this.queriesPerSecond = queriesPerSecond;
- this.writesPerSecond = writesPerSecond;
- this.documentCount = documentCount;
- this.queryLatencyMillis = queryLatencyMillis;
- this.writeLatencyMills = writeLatencyMills;
- this.instant = Objects.requireNonNull(instant, "instant must be non-null");
- this.warnings = Map.copyOf(Objects.requireNonNull(warnings, "warnings must be non-null"));
- if (warnings.entrySet().stream().anyMatch(kv -> kv.getValue() < 0)) {
- throw new IllegalArgumentException("Warning count must be non-negative. Got " + warnings);
- }
- }
-
- /** Returns the number of queries per second */
- public double queriesPerSecond() {
- return queriesPerSecond;
- }
-
- /** Returns the number of writes per second */
- public double writesPerSecond() {
- return writesPerSecond;
- }
-
- /** Returns the number of documents */
- public double documentCount() {
- return documentCount;
- }
-
- /** Returns the average query latency in milliseconds */
- public double queryLatencyMillis() {
- return queryLatencyMillis;
- }
-
- /** Returns the average write latency in milliseconds */
- public double writeLatencyMillis() {
- return writeLatencyMills;
- }
-
- /** Returns the approximate time this was measured */
- public Optional<Instant> instant() {
- return instant;
- }
-
- /** Returns the number of warnings of the most recent deployment */
- public Map<Warning, Integer> warnings() {
- return warnings;
- }
-
- public DeploymentMetrics withQueriesPerSecond(double queriesPerSecond) {
- return new DeploymentMetrics(queriesPerSecond, writesPerSecond, documentCount, queryLatencyMillis,
- writeLatencyMills, instant, warnings);
- }
-
- public DeploymentMetrics withWritesPerSecond(double writesPerSecond) {
- return new DeploymentMetrics(queriesPerSecond, writesPerSecond, documentCount, queryLatencyMillis,
- writeLatencyMills, instant, warnings);
- }
-
- public DeploymentMetrics withDocumentCount(double documentCount) {
- return new DeploymentMetrics(queriesPerSecond, writesPerSecond, documentCount, queryLatencyMillis,
- writeLatencyMills, instant, warnings);
- }
-
- public DeploymentMetrics withQueryLatencyMillis(double queryLatencyMillis) {
- return new DeploymentMetrics(queriesPerSecond, writesPerSecond, documentCount, queryLatencyMillis,
- writeLatencyMills, instant, warnings);
- }
-
- public DeploymentMetrics withWriteLatencyMillis(double writeLatencyMills) {
- return new DeploymentMetrics(queriesPerSecond, writesPerSecond, documentCount, queryLatencyMillis,
- writeLatencyMills, instant, warnings);
- }
-
- public DeploymentMetrics at(Instant instant) {
- return new DeploymentMetrics(queriesPerSecond, writesPerSecond, documentCount, queryLatencyMillis,
- writeLatencyMills, Optional.of(instant), warnings);
- }
-
- public DeploymentMetrics with(Map<Warning, Integer> warnings) {
- return new DeploymentMetrics(queriesPerSecond, writesPerSecond, documentCount, queryLatencyMillis,
- writeLatencyMills, instant, warnings);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- DeploymentMetrics that = (DeploymentMetrics) o;
- return Double.compare(that.queriesPerSecond, queriesPerSecond) == 0 &&
- Double.compare(that.writesPerSecond, writesPerSecond) == 0 &&
- Double.compare(that.documentCount, documentCount) == 0 &&
- Double.compare(that.queryLatencyMillis, queryLatencyMillis) == 0 &&
- Double.compare(that.writeLatencyMills, writeLatencyMills) == 0 &&
- instant.equals(that.instant) &&
- warnings.equals(that.warnings);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(queriesPerSecond, writesPerSecond, documentCount, queryLatencyMillis, writeLatencyMills, instant, warnings);
- }
-
- /** Types of deployment warnings. We currently have only one */
- public enum Warning {
- all
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java
deleted file mode 100644
index 4132b560fae..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculator.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterResources;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
-
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.List;
-
-/**
- * Calculates the quota to allocate to a deployment.
- *
- * @author ogronnesby
- * @author andreer
- */
-public class DeploymentQuotaCalculator {
-
- public static Quota calculate(Quota tenantQuota,
- List<Application> tenantApps,
- ApplicationId deployingApp, ZoneId deployingZone,
- DeploymentSpec deploymentSpec)
- {
- if (tenantQuota.budget().isEmpty()) return tenantQuota; // Shortcut if there is no budget limit to care about.
- if (deployingZone.environment().isTest()) return tenantQuota;
- if (deployingZone.environment().isProduction()) return probablyEnoughForAll(tenantQuota, tenantApps, deployingApp, deploymentSpec);
- return getMaximumAllowedQuota(tenantQuota, tenantApps, deployingApp, deployingZone);
- }
-
- public static QuotaUsage calculateQuotaUsage(com.yahoo.vespa.hosted.controller.api.integration.configserver.Application application) {
- var quotaUsageRate = application.clusters().values().stream()
- .filter(cluster -> ! cluster.type().equals(ClusterSpec.Type.admin))
- .map(cluster -> largestQuotaUsage(cluster.current(), cluster.max()))
- .mapToDouble(resources -> resources.nodes() * resources.nodeResources().cost())
- .sum();
- return QuotaUsage.create(quotaUsageRate);
- }
-
- private static ClusterResources largestQuotaUsage(ClusterResources a, ClusterResources b) {
- return a.cost() > b.cost() ? a : b;
- }
-
- /** Just get the maximum quota we are allowed to use. */
- private static Quota getMaximumAllowedQuota(Quota tenantQuota, List<Application> applications,
- ApplicationId application, ZoneId zone) {
- var usageOutsideDeployment = applications.stream()
- .map(app -> app.quotaUsage(application, zone))
- .reduce(QuotaUsage::add).orElse(QuotaUsage.none);
- return tenantQuota.subtractUsage(usageOutsideDeployment.rate());
- }
-
- /**
- * We want to avoid applying a resource change to an instance in production when it seems likely
- * that there will not be enough quota to apply this change to _all_ production instances.
- * <p>
- * To achieve this, we must make the assumption that all production instances will use
- * the same amount of resources, and so equally divide the quota among them.
- */
- private static Quota probablyEnoughForAll(Quota tenantQuota, List<Application> tenantApps,
- ApplicationId application, DeploymentSpec deploymentSpec) {
-
- TenantAndApplicationId deployingAppId = TenantAndApplicationId.from(application);
-
- var usageOutsideApplication = tenantApps.stream()
- .filter(app -> !app.id().equals(deployingAppId))
- .map(Application::quotaUsage).reduce(QuotaUsage::add).orElse(QuotaUsage.none);
-
- QuotaUsage manualQuotaUsage = tenantApps.stream()
- .filter(app -> app.id().equals(deployingAppId)).findFirst()
- .map(Application::manualQuotaUsage).orElse(QuotaUsage.none);
-
- long productionDeployments = Math.max(1, deploymentSpec.instances().stream()
- .flatMap(instance -> instance.zones().stream())
- .filter(zone -> zone.environment().isProduction())
- .count());
-
- return tenantQuota.withBudget(
- tenantQuota.subtractUsage(usageOutsideApplication.rate() + manualQuotaUsage.rate())
- .budget().get().divide(BigDecimal.valueOf(productionDeployments),
- 5, RoundingMode.HALF_UP)); // 1/1000th of a cent should be accurate enough
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java
deleted file mode 100644
index 39e1c89c202..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java
+++ /dev/null
@@ -1,658 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-
-import java.net.URI;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static java.util.Comparator.comparing;
-
-/**
- * Represents an application or instance endpoint in hosted Vespa.
- * <p>
- * This encapsulates the logic for building URLs and DNS names for applications in all hosted Vespa systems.
- *
- * @author mpolden
- */
-public class Endpoint {
-
- private static final String MAIN_OATH_DNS_SUFFIX = ".vespa.oath.cloud";
- private static final String CD_OATH_DNS_SUFFIX = ".cd.vespa.oath.cloud";
- private static final String PUBLIC_DNS_SUFFIX = ".vespa-app.cloud";
- private static final String PUBLIC_CD_DNS_SUFFIX = ".cd.vespa-app.cloud";
-
- private final EndpointId id;
- private final ClusterSpec.Id cluster;
- private final Optional<InstanceName> instance;
- private final URI url;
- private final List<Target> targets;
- private final Scope scope;
- private final boolean legacy;
- private final RoutingMethod routingMethod;
- private final AuthMethod authMethod;
- private final Optional<GeneratedEndpoint> generated;
-
- private Endpoint(TenantAndApplicationId application, Optional<InstanceName> instanceName, EndpointId id,
- ClusterSpec.Id cluster, URI url, List<Target> targets, Scope scope, Port port, boolean legacy,
- RoutingMethod routingMethod, boolean certificateName, AuthMethod authMethod, Optional<GeneratedEndpoint> generated) {
- Objects.requireNonNull(application, "application must be non-null");
- Objects.requireNonNull(instanceName, "instanceName must be non-null");
- Objects.requireNonNull(cluster, "cluster must be non-null");
- Objects.requireNonNull(url, "url must be non-null");
- Objects.requireNonNull(targets, "deployment must be non-null");
- Objects.requireNonNull(scope, "scope must be non-null");
- Objects.requireNonNull(port, "port must be non-null");
- Objects.requireNonNull(routingMethod, "routingMethod must be non-null");
- Objects.requireNonNull(authMethod, "authMethod must be non-null");
- Objects.requireNonNull(generated, "generated must be non-null");
- this.id = requireEndpointId(id, scope, certificateName);
- this.cluster = requireCluster(cluster, certificateName);
- this.instance = requireInstance(instanceName, scope, certificateName, generated.isPresent());
- this.url = url;
- this.targets = List.copyOf(requireTargets(targets, application, instanceName, scope, certificateName));
- this.scope = requireScope(scope, routingMethod);
- this.legacy = legacy;
- this.routingMethod = routingMethod;
- this.authMethod = authMethod;
- this.generated = generated;
- }
-
- /**
- * Returns the name of this endpoint (the first component of the DNS name). This can be one of the following:
- *
- * - The wildcard character '*' (for wildcard endpoints, with any scope)
- * - The cluster ID ({@link Scope#zone} and {@link Scope#weighted}
- * - The endpoint ID ({@link Scope#global} and {@link Scope#application})
- */
- public String name() {
- return endpointOrClusterAsString(id, cluster);
- }
-
- /** Returns the cluster ID to which this routes traffic */
- public ClusterSpec.Id cluster() {
- return cluster;
- }
-
- /** The specific instance this endpoint points to, if any */
- public Optional<InstanceName> instance() {
- return instance;
- }
-
- /** Returns the URL used to access this */
- public URI url() {
- return url;
- }
-
- /** Returns the DNS name of this */
- public String dnsName() {
- // because getHost returns "null" for wildcard endpoints
- return url.getAuthority().replaceAll(":.*", "");
- }
-
- /** Returns the target(s) to which this routes traffic */
- public List<Target> targets() {
- return targets;
- }
-
- /** Returns the deployments(s) to which this routes traffic */
- public List<DeploymentId> deployments() {
- return targets.stream().map(Target::deployment).toList();
- }
-
- /** Returns the scope of this */
- public Scope scope() {
- return scope;
- }
-
- /** Returns whether this is considered a legacy DNS name intended to be removed at some point */
- public boolean legacy() {
- return legacy;
- }
-
- /** Returns the routing method used for this */
- public RoutingMethod routingMethod() {
- return routingMethod;
- }
-
- /** Returns whether this endpoint supports TLS connections */
- public boolean tls() {
- return true;
- }
-
- /** Returns whether this requires a rotation to be reachable */
- public boolean requiresRotation() {
- return routingMethod.isShared() && scope == Scope.global;
- }
-
- /** Returns whether this endpoint is generated by the system */
- public Optional<GeneratedEndpoint> generated() {
- return generated;
- }
-
- /** Returns the upstream name of given deployment. This *must* match what the routing layer generates */
- public String upstreamName(DeploymentId deployment) {
- if (!routingMethod.isShared()) throw new IllegalArgumentException("Routing method " + routingMethod + " does not have upstream name");
- return upstreamName(cluster.value(), deployment.applicationId(), deployment.zoneId());
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Endpoint endpoint = (Endpoint) o;
- return url.equals(endpoint.url);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(url);
- }
-
- @Override
- public String toString() {
- return Text.format("endpoint %s [scope=%s, legacy=%s, routingMethod=%s, authMethod=%s, name=%s]", url, scope, legacy, routingMethod, authMethod, name());
- }
-
- private static String endpointOrClusterAsString(EndpointId id, ClusterSpec.Id cluster) {
- return id == null ? cluster.value() : id.id();
- }
-
- private static URI createUrl(String name, TenantAndApplicationId application, Optional<InstanceName> instance,
- List<Target> targets, Scope scope, SystemName system, Port port,
- Optional<GeneratedEndpoint> generated) {
-
- String separator = ".";
- String portPart = port.isDefault() ? "" : ":" + port.port;
- final String subdomain;
- if (generated.isPresent()) {
- subdomain = generatedPart(generated.get(), separator);
- } else {
- subdomain = sanitize(namePart(name, separator)) +
- systemPart(system, separator) +
- sanitize(instancePart(instance, separator)) +
- sanitize(application.application().value()) +
- separator +
- sanitize(application.tenant().value());
- }
- return URI.create("https://" +
- subdomain +
- "." +
- scopePart(scope, targets, system, generated) +
- dnsSuffix(system) +
- portPart +
- "/");
- }
-
- private static String generatedPart(GeneratedEndpoint generated, String separator) {
- return generated.clusterPart() + separator + generated.applicationPart();
- }
-
- private static String sanitize(String part) { // TODO: Reject reserved words
- return part.replace('_', '-');
- }
-
- private static String namePart(String name, String separator) {
- if ("default".equals(name)) return "";
- return name + separator;
- }
-
- private static String scopePart(Scope scope, List<Target> targets, SystemName system, Optional<GeneratedEndpoint> generated) {
- String scopeSymbol = scopeSymbol(scope, system, generated);
- if (scope == Scope.global) return scopeSymbol;
- if (scope == Scope.application) return scopeSymbol;
- if (scope == Scope.zone && generated.isPresent()) return scopeSymbol;
-
- ZoneId zone = targets.stream().map(target -> target.deployment.zoneId()).min(comparing(ZoneId::value)).get();
- String region = zone.region().value();
- String environment = zone.environment().isProduction() ? "" : "." + zone.environment().value();
- if (system.isPublic()) {
- return region + environment + "." + scopeSymbol;
- }
- return region + (scopeSymbol.isEmpty() ? "" : "-" + scopeSymbol) + environment;
- }
-
- private static String scopeSymbol(Scope scope, SystemName system, Optional<GeneratedEndpoint> generated) {
- if (system.isPublic() || generated.isPresent()) {
- return switch (scope) {
- case zone -> "z";
- case weighted -> "w";
- case global -> "g";
- case application -> "a";
- };
- }
- return switch (scope) {
- case zone -> "";
- case weighted -> "w";
- case global -> "global";
- case application -> "a";
- };
- }
-
- private static String instancePart(Optional<InstanceName> instance, String separator) {
- if (instance.isEmpty()) return "";
- if (instance.get().isDefault()) return ""; // Skip "default"
- return instance.get().value() + separator;
- }
-
- private static String systemPart(SystemName system, String separator) {
- if (!system.isCd()) return "";
- if (system.isPublic()) return "";
- return system.value() + separator;
- }
-
- /** Returns the DNS suffix used for endpoints in given system */
- private static String dnsSuffix(SystemName system) {
- return switch (system) {
- case cd -> CD_OATH_DNS_SUFFIX;
- case main -> MAIN_OATH_DNS_SUFFIX;
- case Public -> PUBLIC_DNS_SUFFIX;
- case PublicCd -> PUBLIC_CD_DNS_SUFFIX;
- default -> throw new IllegalArgumentException("No DNS suffix declared for system " + system);
- };
- }
-
- /** Returns the DNS suffix used for internal names (i.e. names not exposed to tenants) in given system */
- public static String internalDnsSuffix(SystemName system) {
- String suffix = dnsSuffix(system);
- if (system.isPublic()) {
- // Certificate provider requires special approval for three-level DNS names, e.g. foo.vespa-app.cloud.
- // To avoid this in public we always add an extra level.
- return ".internal" + suffix;
- }
- return suffix;
- }
-
- private static String upstreamName(String name, ApplicationId application, ZoneId zone) {
- return Stream.of(namePart(name, ""),
- instancePart(Optional.of(application.instance()), ""),
- application.application().value(),
- application.tenant().value(),
- zone.region().value(),
- zone.environment().value())
- .filter(Predicate.not(String::isEmpty))
- .map(Endpoint::sanitizeUpstream)
- .collect(Collectors.joining("."));
- }
-
- /** Remove any invalid characters from a upstream part */
- private static String sanitizeUpstream(String part) {
- return truncate(part.toLowerCase()
- .replace('_', '-')
- .replaceAll("[^a-z0-9-]*", ""));
- }
-
- /** Truncate the given part at the front so its length does not exceed 63 characters */
- private static String truncate(String part) {
- return part.substring(Math.max(0, part.length() - 63));
- }
-
- private static ClusterSpec.Id requireCluster(ClusterSpec.Id cluster, boolean certificateName) {
- if (!certificateName && cluster.value().equals("*")) throw new IllegalArgumentException("Wildcard found in cluster ID which is not a certificate name");
- return cluster;
- }
-
- private static EndpointId requireEndpointId(EndpointId endpointId, Scope scope, boolean certificateName) {
- if (scope.multiDeployment() && endpointId == null) throw new IllegalArgumentException("Endpoint ID must be set for multi-deployment endpoints");
- if (scope == Scope.zone && endpointId != null) throw new IllegalArgumentException("Endpoint ID cannot be set for " + scope + " endpoints");
- if (!certificateName && endpointId != null && endpointId.id().equals("*")) throw new IllegalArgumentException("Wildcard found in endpoint ID which is not a certificate name");
- return endpointId;
- }
-
- private static Optional<InstanceName> requireInstance(Optional<InstanceName> instanceName, Scope scope, boolean certificateName, boolean generated) {
- if (generated && certificateName) {
- return instanceName;
- }
- if (scope == Scope.application) {
- if (instanceName.isPresent()) throw new IllegalArgumentException("Instance cannot be set for scope " + scope);
- } else {
- if (instanceName.isEmpty()) throw new IllegalArgumentException("Instance must be set for scope " + scope);
- }
- return instanceName;
- }
-
- private static Scope requireScope(Scope scope, RoutingMethod routingMethod) {
- if (scope == Scope.application && !routingMethod.isDirect()) throw new IllegalArgumentException("Routing method " + routingMethod + " does not support " + scope + "-scoped endpoints");
- return scope;
- }
-
- private static List<Target> requireTargets(List<Target> targets, TenantAndApplicationId application, Optional<InstanceName> instanceName, Scope scope, boolean certificateName) {
- if (certificateName && targets.isEmpty()) return List.of();
- if (targets.isEmpty()) throw new IllegalArgumentException("At least one target must be given for " + scope + " endpoints");
- if (scope == Scope.zone && targets.size() != 1) throw new IllegalArgumentException("Exactly one target must be given for " + scope + " endpoints");
- for (var target : targets) {
- if (scope == Scope.application) {
- TenantAndApplicationId owner = TenantAndApplicationId.from(target.deployment().applicationId());
- if (!owner.equals(application)) {
- throw new IllegalArgumentException("Endpoint has target owned by " + owner +
- ", which does not match application of this endpoint: " +
- application);
- }
- } else {
- ApplicationId owner = target.deployment.applicationId();
- ApplicationId instance = application.instance(instanceName.get());
- if (!owner.equals(instance)) {
- throw new IllegalArgumentException("Endpoint has target owned by " + owner +
- ", which does not match instance of this endpoint: " + instance);
- }
- }
- }
- return targets;
- }
-
- /** Returns the authentication method of this endpoint */
- public AuthMethod authMethod() {
- return authMethod;
- }
-
- /** An endpoint's scope */
- public enum Scope {
-
- /**
- * Endpoint points to a multiple instances of an application, in the same region.
- *
- * Traffic is routed across instances according to weights specified in deployment.xml
- */
- application,
-
- /** Endpoint points to one or more zones. Traffic is routed to the zone closest to the client */
- global,
-
- /**
- * Endpoint points to one more zones in the same geographical region. Traffic is routed evenly across zones.
- *
- * This is for internal use only. Endpoints with this scope are not exposed directly to tenants.
- */
- weighted,
-
- /** Endpoint points to a single zone */
- zone;
-
- /** Returns whether this scope may span multiple deployments */
- public boolean multiDeployment() {
- return this == application || this == global;
- }
-
- }
-
- /** Represents an endpoint's HTTP port */
- public record Port(int port) {
-
- private static final Port TLS_DEFAULT = new Port(443);
-
- public Port {
- if (port < 1 || port > 65535) {
- throw new IllegalArgumentException("Port must be between 1 and 65535, got " + port);
- }
- }
-
- private boolean isDefault() {
- return port == TLS_DEFAULT.port;
- }
-
- /** Returns the default HTTPS port */
- public static Port tls() {
- return TLS_DEFAULT;
- }
-
- /** Returns default port for the given routing method */
- public static Port fromRoutingMethod(RoutingMethod method) {
- if (method.isDirect()) return Port.tls();
- return new Port(4443);
- }
-
- }
-
- /** Build an endpoint for given instance */
- public static EndpointBuilder of(ApplicationId instance) {
- return new EndpointBuilder(TenantAndApplicationId.from(instance), Optional.of(instance.instance()));
- }
-
- /** Build an endpoint for given application */
- public static EndpointBuilder of(TenantAndApplicationId application) {
- return new EndpointBuilder(application, Optional.empty());
- }
-
- /** A target of an endpoint */
- public static class Target {
-
- private final DeploymentId deployment;
- private final int weight;
-
- private Target(DeploymentId deployment, int weight) {
- this.deployment = Objects.requireNonNull(deployment);
- this.weight = weight;
- if (weight < 0 || weight > 100) {
- throw new IllegalArgumentException("Endpoint target weight must be in range [0, 100], got " + weight);
- }
- }
-
- private Target(DeploymentId deployment) {
- this(deployment, 1);
- }
-
- /** Returns the deployment of this */
- public DeploymentId deployment() {
- return deployment;
- }
-
- /** Returns the assigned weight of this */
- public int weight() {
- return weight;
- }
-
- /** Returns whether this routes to given deployment */
- public boolean routesTo(DeploymentId deployment) {
- return this.deployment.equals(deployment);
- }
-
- }
-
- public static class EndpointBuilder {
-
- private final TenantAndApplicationId application;
- private final Optional<InstanceName> instance;
-
- private Scope scope;
- private List<Target> targets;
- private ClusterSpec.Id cluster;
- private EndpointId endpointId;
- private Port port;
- private RoutingMethod routingMethod = RoutingMethod.sharedLayer4;
- private boolean legacy = false;
- private boolean certificateName = false;
- private AuthMethod authMethod = AuthMethod.mtls;
- private Optional<GeneratedEndpoint> generated = Optional.empty();
-
- private EndpointBuilder(TenantAndApplicationId application, Optional<InstanceName> instance) {
- this.application = Objects.requireNonNull(application);
- this.instance = Objects.requireNonNull(instance);
- }
-
- /** Sets the zone target for this */
- public EndpointBuilder target(ClusterSpec.Id cluster, DeploymentId deployment) {
- this.cluster = cluster;
- this.scope = requireUnset(Scope.zone);
- this.targets = List.of(new Target(deployment));
- return this;
- }
-
- /** Sets the global target with given ID, deployments and cluster (as defined in deployments.xml) */
- public EndpointBuilder target(EndpointId endpointId, ClusterSpec.Id cluster, List<DeploymentId> deployments) {
- this.endpointId = endpointId;
- this.cluster = cluster;
- this.targets = deployments.stream().map(Target::new).toList();
- this.scope = requireUnset(Scope.global);
- return this;
- }
-
- /** Sets the global target with given ID and pointing to the default cluster */
- public EndpointBuilder target(EndpointId endpointId) {
- return target(endpointId, ClusterSpec.Id.from("default"), List.of());
- }
-
- /** Sets the application target with given ID and pointing to the default cluster */
- public EndpointBuilder targetApplication(EndpointId endpointId, DeploymentId deployment) {
- return targetApplication(endpointId, ClusterSpec.Id.from("default"), Map.of(deployment, 1));
- }
-
- /** Sets the global wildcard target for this */
- public EndpointBuilder wildcard() {
- return target(EndpointId.of("*"), ClusterSpec.Id.from("*"), List.of());
- }
-
- /** Sets the application wildcard target for this */
- public EndpointBuilder wildcardApplication(DeploymentId deployment) {
- return targetApplication(EndpointId.of("*"), ClusterSpec.Id.from("*"), Map.of(deployment, 1));
- }
-
- /** Sets the zone wildcard target for this */
- public EndpointBuilder wildcard(DeploymentId deployment) {
- return target(ClusterSpec.Id.from("*"), deployment);
- }
-
- /** Sets the generated wildcard target for this */
- public EndpointBuilder wildcardGenerated(String applicationPart, Scope scope) {
- this.cluster = ClusterSpec.Id.from("*");
- if (scope.multiDeployment()) {
- this.endpointId = EndpointId.of("*");
- }
- this.targets = List.of();
- this.scope = requireUnset(scope);
- this.generated = Optional.of(new GeneratedEndpoint("*", applicationPart, AuthMethod.mtls, Optional.ofNullable(endpointId)));
- return this;
- }
-
- /** Sets the application target with given ID, cluster, deployments and their weights */
- public EndpointBuilder targetApplication(EndpointId endpointId, ClusterSpec.Id cluster, Map<DeploymentId, Integer> deployments) {
- this.endpointId = endpointId;
- this.cluster = cluster;
- this.targets = deployments.entrySet().stream()
- .map(kv -> new Target(kv.getKey(), kv.getValue()))
- .toList();
- this.scope = Scope.application;
- return this;
- }
-
- /** Sets the region target for this, deduced from given zone */
- public EndpointBuilder targetRegion(ClusterSpec.Id cluster, String cloudNativeRegion, CloudName cloudName) {
- this.cluster = cluster;
- this.scope = requireUnset(Scope.weighted);
- RegionName region = RegionName.from(cloudName.value() + "-" + cloudNativeRegion);
- this.targets = List.of(new Target(new DeploymentId(application.instance(instance.get()), ZoneId.from(Environment.prod, region))));
- this.authMethod = AuthMethod.none;
- return this;
- }
-
- /** Sets the valid authentication method supported by this */
- public EndpointBuilder authMethod(AuthMethod authMethod) {
- this.authMethod = authMethod;
- return this;
- }
-
- /** Sets the port of this */
- public EndpointBuilder on(Port port) {
- this.port = port;
- return this;
- }
-
- /** Set whether this is a legacy endpoint */
- public EndpointBuilder legacy(boolean legacy) {
- this.legacy = legacy;
- return this;
- }
-
- /** Sets the routing method for this */
- public EndpointBuilder routingMethod(RoutingMethod method) {
- this.routingMethod = method;
- return this;
- }
-
- /** Sets whether we're building a name for inclusion in a certificate */
- public EndpointBuilder certificateName() {
- this.certificateName = true;
- return this;
- }
-
- /** Sets the generated ID to use when building this */
- public EndpointBuilder generatedFrom(GeneratedEndpoint generated) {
- this.generated = Optional.of(generated);
- return this;
- }
-
- /** Sets the system that owns this */
- public Endpoint in(SystemName system) {
- String name = endpointOrClusterAsString(endpointId, Objects.requireNonNull(cluster, "cluster must be non-null"));
- URI url = createUrl(name,
- Objects.requireNonNull(application, "application must be non-null"),
- Objects.requireNonNull(instance, "instance must be non-null"),
- Objects.requireNonNull(targets, "targets must be non-null"),
- Objects.requireNonNull(scope, "scope must be non-null"),
- Objects.requireNonNull(system, "system must be non-null"),
- Objects.requireNonNull(port, "port must be non-null"),
- Objects.requireNonNull(generated)
- );
- if (system.isPublic() && routingMethod != RoutingMethod.exclusive) {
- throw illegal(url, "Public system only supports routing method " + RoutingMethod.exclusive + ", got " + routingMethod);
- }
- if (routingMethod.isDirect() && !port.isDefault()) {
- throw illegal(url, "Routing method " + routingMethod + " can only use default port, got " + port);
- }
- if (authMethod == AuthMethod.token && generated.isEmpty()) {
- throw illegal(url, authMethod + " is only supported for generated endpoints");
- }
- if (scope != Scope.weighted && generated.isPresent() && generated.get().authMethod() != authMethod) {
- throw illegal(url, "Authentication method of " + scope + " endpoint does not match authentication method of generated endpoint: " + generated.get().authMethod());
- }
- if ((scope == Scope.weighted) != (authMethod == AuthMethod.none)) {
- throw illegal(url, "Attempted to set unsupported authentication method " + authMethod + " on " + scope + " endpoint");
- }
- if (scope.multiDeployment() && generated.isPresent() && (generated.get().endpoint().isEmpty() || !generated.get().endpoint().get().equals(endpointId))) {
- throw illegal(url, "Generated endpoint must contain a matching endpoint ID, but got " + generated.get().endpoint());
- }
- return new Endpoint(application,
- instance,
- endpointId,
- cluster,
- url,
- targets,
- scope,
- port,
- legacy,
- routingMethod,
- certificateName,
- authMethod,
- generated);
- }
-
- private static IllegalArgumentException illegal(URI url, String reason) {
- return new IllegalArgumentException("Invalid endpoint: " + url + ": " + reason);
- }
-
- private Scope requireUnset(Scope scope) {
- if (this.scope != null) {
- throw new IllegalArgumentException("Cannot change endpoint scope. Already set to " + scope);
- }
- return scope;
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointId.java
deleted file mode 100644
index ef1f43eee69..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointId.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import java.util.Objects;
-
-/**
- * A user-specified endpoint ID. This is typically the first part of an endpoint name.
- *
- * @author ogronnesby
- */
-public class EndpointId implements Comparable<EndpointId> {
-
- private static final EndpointId DEFAULT = new EndpointId("default");
-
- private final String id;
-
- private EndpointId(String id) {
- this.id = requireNotEmpty(id);
- }
-
- public String id() { return id; }
-
- @Override
- public String toString() {
- return "endpoint id '" + id + "'";
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- EndpointId that = (EndpointId) o;
- return Objects.equals(id, that.id);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id);
- }
-
- private static String requireNotEmpty(String input) {
- Objects.requireNonNull(input);
- if (input.isEmpty()) {
- throw new IllegalArgumentException("The value EndpointId was empty");
- }
- return input;
- }
-
- public static EndpointId defaultId() { return DEFAULT; }
-
- public static EndpointId of(String id) { return new EndpointId(id); }
-
- @Override
- public int compareTo(EndpointId o) {
- return id.compareTo(o.id);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java
deleted file mode 100644
index 07fd6d9825d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/EndpointList.java
+++ /dev/null
@@ -1,111 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.collections.AbstractFilteringList;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-
-
-/**
- * A list of endpoints for an application.
- *
- * @author mpolden
- */
-public class EndpointList extends AbstractFilteringList<Endpoint, EndpointList> {
-
- public static final EndpointList EMPTY = EndpointList.copyOf(List.of());
-
- private EndpointList(Collection<? extends Endpoint> endpoints, boolean negate) {
- super(endpoints, negate, EndpointList::new);
- if (endpoints.stream().distinct().count() != endpoints.size()) {
- throw new IllegalArgumentException("Expected all endpoints to be distinct, got " + endpoints);
- }
- }
-
- /** Returns the subset of endpoints named according to given ID and scope */
- public EndpointList named(EndpointId id, Endpoint.Scope scope) {
- return matching(endpoint -> endpoint.scope() == scope && // ID is only unique within a scope
- endpoint.name().equals(id.id()));
- }
-
- /** Returns the endpoint which has given DNS name, if any */
- public Optional<Endpoint> dnsName(String dnsName) {
- return matching(endpoint -> endpoint.dnsName().equals(dnsName)).first();
- }
-
- /** Returns the subset of endpoints pointing to given cluster */
- public EndpointList cluster(ClusterSpec.Id cluster) {
- return matching(endpoint -> endpoint.cluster().equals(cluster));
- }
-
- /** Returns the subset of endpoints pointing to given instance */
- public EndpointList instance(InstanceName instance) {
- return matching(endpoint -> endpoint.instance().isPresent() &&
- endpoint.instance().get().equals(instance));
- }
-
- /** Returns the subset of endpoints which target all the given deployments */
- public EndpointList targets(List<DeploymentId> deployments) {
- return matching(endpoint -> endpoint.deployments().containsAll(deployments));
- }
-
- /** Returns the subset of endpoints which target the given deployment */
- public EndpointList targets(DeploymentId deployment) {
- return targets(List.of(deployment));
- }
-
- /** Returns the subset of endpoints that are considered legacy */
- public EndpointList legacy() {
- return matching(Endpoint::legacy);
- }
-
- /** Returns the subset of endpoints generated by the system */
- public EndpointList generated() {
- return matching(endpoint -> endpoint.generated().isPresent());
- }
-
- /** Returns the subset of endpoints that require a rotation */
- public EndpointList requiresRotation() {
- return matching(Endpoint::requiresRotation);
- }
-
- /** Returns the subset of endpoints with given scope */
- public EndpointList scope(Endpoint.Scope scope) {
- return matching(endpoint -> endpoint.scope() == scope);
- }
-
- /** Returns the subset of endpoints that use direct routing */
- public EndpointList direct() {
- return matching(endpoint -> endpoint.routingMethod().isDirect());
- }
-
- /** Returns the subset of endpoints that use shared routing */
- public EndpointList shared() {
- return matching(endpoint -> endpoint.routingMethod().isShared());
- }
-
- /** Returns the subset of endpoints supporting given authentication method */
- public EndpointList authMethod(AuthMethod authMethod) {
- return matching(endpoint -> endpoint.authMethod() == authMethod);
- }
-
- public static EndpointList copyOf(Collection<Endpoint> endpoints) {
- return new EndpointList(endpoints, false);
- }
-
- public static EndpointList of(Endpoint ...endpoint) {
- return copyOf(List.of(endpoint));
- }
-
- @Override
- public String toString() {
- return asList().toString();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/GeneratedEndpoint.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/GeneratedEndpoint.java
deleted file mode 100644
index 5f75d6105b5..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/GeneratedEndpoint.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import ai.vespa.validation.Validation;
-import com.yahoo.config.provision.zone.AuthMethod;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.random.RandomGenerator;
-import java.util.regex.Pattern;
-
-/**
- * A system-generated endpoint, where the cluster and application parts are randomly generated. These become the
- * first and second part of an endpoint name. See {@link Endpoint}.
- *
- * @author mpolden
- */
-public record GeneratedEndpoint(String clusterPart, String applicationPart, AuthMethod authMethod, Optional<EndpointId> endpoint) {
-
- private static final Pattern CLUSTER_PART_PATTERN = Pattern.compile("^([a-f][a-f0-9]{7}|\\*)$");
- private static final Pattern APPLICATION_PART_PATTERN = Pattern.compile("^[a-f][a-f0-9]{7}$");
-
- public GeneratedEndpoint {
- Objects.requireNonNull(clusterPart);
- Objects.requireNonNull(applicationPart);
- Objects.requireNonNull(authMethod);
- Objects.requireNonNull(endpoint);
-
- Validation.requireMatch(clusterPart, "Cluster part", CLUSTER_PART_PATTERN);
- Validation.requireMatch(applicationPart, "Application part", APPLICATION_PART_PATTERN);
- }
-
- /** Returns whether this was generated for an endpoint declared in {@link com.yahoo.config.application.api.DeploymentSpec} */
- public boolean declared() {
- return endpoint.isPresent();
- }
-
- /** Returns whether this was generated for a cluster declared in {@link com.yahoo.vespa.hosted.controller.application.pkg.BasicServicesXml} */
- public boolean cluster() {
- return !declared();
- }
-
- /** Returns a copy of this with cluster part set to given value */
- public GeneratedEndpoint withClusterPart(String clusterPart) {
- return new GeneratedEndpoint(clusterPart, applicationPart, authMethod, endpoint);
- }
-
- /** Create a new endpoint part, using random as a source of randomness */
- public static String createPart(RandomGenerator random) {
- String alphabet = "abcdef0123456789";
- StringBuilder sb = new StringBuilder();
- sb.append(alphabet.charAt(random.nextInt(6))); // Start with letter
- for (int i = 0; i < 7; i++) {
- sb.append(alphabet.charAt(random.nextInt(alphabet.length())));
- }
- return sb.toString();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java
deleted file mode 100644
index 939b3df9502..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/InstanceList.java
+++ /dev/null
@@ -1,190 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.collections.AbstractFilteringList;
-import com.yahoo.component.Version;
-import com.yahoo.component.VersionCompatibility;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatusList;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-
-import java.time.Instant;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Function;
-
-import static java.util.Comparator.comparing;
-import static java.util.Comparator.naturalOrder;
-
-/**
- * @author jonmv
- */
-public class InstanceList extends AbstractFilteringList<ApplicationId, InstanceList> {
-
- private final Map<ApplicationId, DeploymentStatus> instances;
-
- private InstanceList(Collection<? extends ApplicationId> items, boolean negate, Map<ApplicationId, DeploymentStatus> instances) {
- super(items, negate, (i, n) -> new InstanceList(i, n, instances));
- this.instances = Map.copyOf(instances);
- }
-
- /**
- * Returns the subset of instances where all production deployments are compatible with the given version,
- * and at least one known build is compatible with the given version.
- *
- * @param platform the version which applications returned are compatible with
- */
- public InstanceList compatibleWithPlatform(Version platform, Function<ApplicationId, VersionCompatibility> compatibility) {
- return matching(id -> instance(id).productionDeployments().values().stream()
- .flatMap(deployment -> application(id).revisions().get(deployment.revision()).compileVersion().stream())
- .noneMatch(version -> compatibility.apply(id).refuse(platform, version))
- && application(id).revisions().production().stream()
- .anyMatch(revision -> revision.compileVersion()
- .map(compiled -> compatibility.apply(id).accept(platform, compiled))
- .orElse(true)));
- }
-
- /**
- * Returns the subset of instances whose application have a deployment on the given major,
- * or specify it in deployment spec,
- * or which are on a {@link VespaVersion.Confidence#legacy} platform, and do not specify that in deployment spec.
- *
- * @param targetMajorVersion the target major version which applications returned allows upgrading to
- */
- public InstanceList allowingMajorVersion(int targetMajorVersion, VersionStatus versions) {
- return matching(id -> {
- Application application = application(id);
- Optional<Integer> majorVersion = application.deploymentSpec().majorVersion();
- if (majorVersion.isPresent())
- return majorVersion.get() >= targetMajorVersion;
-
- for (List<Deployment> deployments : application.productionDeployments().values())
- for (Deployment deployment : deployments) {
- if (deployment.version().getMajor() >= targetMajorVersion) return true;
- if (versions.version(deployment.version()).confidence() == Confidence.legacy) return true;
- }
- return false;
- });
- }
-
- /** Returns the subset of instances that are allowed to upgrade to the given version at the given time */
- public InstanceList canUpgradeAt(Version version, Instant instant) {
- return matching(id -> instances.get(id).instanceSteps().get(id.instance())
- .readiness(Change.of(version)).okAt(instant));
- }
-
- /** Returns the subset of instances which have at least one production deployment */
- public InstanceList withProductionDeployment() {
- return matching(id -> instance(id).productionDeployments().size() > 0);
- }
-
- /** Returns the subset of instances which contain declared jobs */
- public InstanceList withDeclaredJobs() {
- return matching(id -> instances.get(id).application().revisions().last().isPresent()
- && instances.get(id).jobSteps().values().stream()
- .anyMatch(job -> job.isDeclared() && job.job().get().application().equals(id)));
- }
-
- /** Returns the subset of instances which have at least one deployment on a lower version than the given one, or which have no production deployments */
- public InstanceList onLowerVersionThan(Version version) {
- return matching(id -> instance(id).productionDeployments().isEmpty()
- || instance(id).productionDeployments().values().stream()
- .anyMatch(deployment -> deployment.version().isBefore(version)));
- }
-
- /** Returns the subset of instances that has completed deployment of given change */
- public InstanceList hasCompleted(Change change) {
- return matching(id -> instances.get(id).hasCompleted(id.instance(), change));
- }
-
- /** Returns the subset of instances which are currently deploying a change */
- public InstanceList deploying() {
- return matching(id -> instance(id).change().hasTargets());
- }
-
- /** Returns the subset of instances which are currently deploying a new revision */
- public InstanceList changingRevision() {
- return matching(id -> instance(id).change().revision().isPresent());
- }
-
- /** Returns the subset of instances which currently have failing jobs on the given version */
- public InstanceList failingOn(Version version) {
- return matching(id -> ! instances.get(id).instanceJobs().get(id).failingHard()
- .lastCompleted().on(version).isEmpty());
- }
-
- /** Returns the subset of instances which are not pinned to a certain Vespa version. */
- public InstanceList unpinned() {
- return matching(id -> ! instance(id).change().isPlatformPinned());
- }
-
- /** Returns the subset of instances which are currently failing a job. */
- public InstanceList failing() {
- return matching(id -> ! instances.get(id).instanceJobs().get(id).failingHard().isEmpty());
- }
-
- /** Returns the subset of instances which are currently failing an upgrade. */
- public InstanceList failingUpgrade() {
- return matching(id -> ! instances.get(id).instanceJobs().get(id).failingHard().not().failingApplicationChange().isEmpty());
- }
-
- /** Returns the subset of instances which are upgrading (to any version), not considering block windows. */
- public InstanceList upgrading() {
- return matching(id -> instance(id).change().platform().isPresent());
- }
-
- /** Returns the subset of instances which are currently upgrading to the given version */
- public InstanceList upgradingTo(Version version) {
- return upgradingTo(List.of(version));
- }
-
-
- /** Returns the subset of instances which are currently upgrading to the given version */
- public InstanceList upgradingTo(Collection<Version> versions) {
- return matching(id -> versions.stream().anyMatch(version -> instance(id).change().platform().equals(Optional.of(version))));
- }
-
- public InstanceList with(DeploymentSpec.UpgradePolicy policy) {
- return matching(id -> application(id).deploymentSpec().requireInstance(id.instance()).upgradePolicy() == policy);
- }
-
- /** Returns the subset of instances which started failing on the given version */
- public InstanceList startedFailingOn(Version version) {
- return matching(id -> ! instances.get(id).instanceJobs().get(id).firstFailing().on(version).isEmpty());
- }
-
- /** Returns this list sorted by increasing oldest production deployment version. Applications without any deployments are ordered first. */
- public InstanceList byIncreasingDeployedVersion() {
- return sortedBy(comparing(id -> instance(id).productionDeployments().values().stream()
- .map(Deployment::version)
- .min(naturalOrder())
- .orElse(Version.emptyVersion)));
- }
-
- private Application application(ApplicationId id) {
- return instances.get(id).application();
- }
-
- private Instance instance(ApplicationId id) {
- return application(id).require(id.instance());
- }
-
- public static InstanceList from(DeploymentStatusList statuses) {
- Map<ApplicationId, DeploymentStatus> instances = new HashMap<>();
- for (DeploymentStatus status : statuses.asList())
- for (InstanceName instance : status.application().deploymentSpec().instanceNames())
- instances.put(status.application().id().instance(instance), status);
- return new InstanceList(instances.keySet(), false, instances);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java
deleted file mode 100644
index 9ff3206ee06..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/MailVerifier.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.TenantController;
-import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer;
-import com.yahoo.vespa.hosted.controller.notification.MailTemplating;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-
-import java.time.Clock;
-import java.time.Duration;
-import java.util.List;
-import java.util.Optional;
-import java.util.UUID;
-
-
-/**
- * @author olaa
- */
-public class MailVerifier {
-
- private static final Duration VERIFICATION_DEADLINE = Duration.ofDays(7);
-
- private final TenantController tenantController;
- private final Mailer mailer;
- private final CuratorDb curatorDb;
- private final Clock clock;
- private final MailTemplating mailTemplating;
-
- public MailVerifier(ConsoleUrls consoleUrls, TenantController tenantController, Mailer mailer, CuratorDb curatorDb, Clock clock) {
- this.tenantController = tenantController;
- this.mailer = mailer;
- this.curatorDb = curatorDb;
- this.clock = clock;
- this.mailTemplating = new MailTemplating(consoleUrls);
- }
-
- public PendingMailVerification sendMailVerification(TenantName tenantName, String email, PendingMailVerification.MailType mailType) {
- if (!email.contains("@")) {
- throw new IllegalArgumentException("Invalid email address");
- }
-
- var verificationCode = UUID.randomUUID().toString();
- var verificationDeadline = clock.instant().plus(VERIFICATION_DEADLINE);
- var pendingMailVerification = new PendingMailVerification(tenantName, email, verificationCode, verificationDeadline, mailType);
- writePendingVerification(pendingMailVerification);
- mailer.send(mailOf(pendingMailVerification));
- return pendingMailVerification;
- }
-
- public Optional<PendingMailVerification> resendMailVerification(TenantName tenantName, String email, PendingMailVerification.MailType mailType) {
- var oldPendingVerification = curatorDb.listPendingMailVerifications()
- .stream()
- .filter(pendingMailVerification ->
- pendingMailVerification.getMailAddress().equals(email) &&
- pendingMailVerification.getMailType().equals(mailType) &&
- pendingMailVerification.getTenantName().equals(tenantName)
- ).findFirst();
-
- if (oldPendingVerification.isEmpty())
- return Optional.empty();
-
- try (var lock = curatorDb.lockPendingMailVerification(oldPendingVerification.get().getVerificationCode())) {
- curatorDb.deletePendingMailVerification(oldPendingVerification.get());
- }
-
- return Optional.of(sendMailVerification(tenantName, email, mailType));
- }
-
- public boolean verifyMail(String verificationCode) {
- return curatorDb.getPendingMailVerification(verificationCode)
- .filter(pendingMailVerification -> pendingMailVerification.getVerificationDeadline().isAfter(clock.instant()))
- .map(pendingMailVerification -> {
- var tenant = requireCloudTenant(pendingMailVerification.getTenantName());
- var oldTenantInfo = tenant.info();
- var updatedTenantInfo = switch (pendingMailVerification.getMailType()) {
- case NOTIFICATIONS -> withTenantContacts(oldTenantInfo, pendingMailVerification);
- case TENANT_CONTACT -> oldTenantInfo.withContact(oldTenantInfo.contact()
- .withEmail(oldTenantInfo.contact().email().withVerification(true)));
- case BILLING -> withVerifiedBillingMail(oldTenantInfo);
- };
-
- tenantController.lockOrThrow(tenant.name(), LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withInfo(updatedTenantInfo);
- tenantController.store(lockedTenant);
- });
-
- try (var lock = curatorDb.lockPendingMailVerification(pendingMailVerification.getVerificationCode())) {
- curatorDb.deletePendingMailVerification(pendingMailVerification);
- }
- return true;
- }).orElse(false);
- }
-
- private TenantInfo withTenantContacts(TenantInfo oldInfo, PendingMailVerification pendingMailVerification) {
- var newContacts = oldInfo.contacts().ofType(TenantContacts.EmailContact.class)
- .stream()
- .map(contact -> {
- if (pendingMailVerification.getMailAddress().equals(contact.email().getEmailAddress()))
- return contact.withEmail(contact.email().withVerification(true));
- return contact;
- }).toList();
- return oldInfo.withContacts(new TenantContacts(newContacts));
- }
-
- private TenantInfo withVerifiedBillingMail(TenantInfo oldInfo) {
- var verifiedMail = oldInfo.billingContact().contact().email().withVerification(true);
- var billingContact = oldInfo.billingContact()
- .withContact(oldInfo.billingContact().contact().withEmail(verifiedMail));
- return oldInfo.withBilling(billingContact);
- }
-
- private void writePendingVerification(PendingMailVerification pendingMailVerification) {
- try (var lock = curatorDb.lockPendingMailVerification(pendingMailVerification.getVerificationCode())) {
- curatorDb.writePendingMailVerification(pendingMailVerification);
- }
- }
-
- private CloudTenant requireCloudTenant(TenantName tenantName) {
- return tenantController.get(tenantName)
- .filter(tenant -> tenant.type() == Tenant.Type.cloud)
- .map(CloudTenant.class::cast)
- .orElseThrow(() -> new IllegalStateException("Mail verification is only applicable for cloud tenants"));
- }
-
- private Mail mailOf(PendingMailVerification pendingMailVerification) {
- var message = mailTemplating.generateMailVerificationHtml(pendingMailVerification);
- return new Mail(List.of(pendingMailVerification.getMailAddress()), "Please verify your email", "", message);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java
deleted file mode 100644
index f5642f44485..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/QuotaUsage.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import java.util.Objects;
-import java.util.OptionalDouble;
-
-/**
- * @author ogronnesby
- */
-public class QuotaUsage {
-
- public static final QuotaUsage none = new QuotaUsage(0.0);
-
- private final double rate;
-
- private QuotaUsage(double rate) {
- this.rate = rate;
- }
-
- public double rate() {
- return rate;
- }
-
- public QuotaUsage add(QuotaUsage addend) {
- return create(rate + addend.rate);
- }
-
- public QuotaUsage sub(QuotaUsage subtrahend) {
- return create(rate - subtrahend.rate);
- }
-
- public static QuotaUsage create(OptionalDouble rate) {
- if (rate.isEmpty()) {
- return QuotaUsage.none;
- }
- return new QuotaUsage(rate.getAsDouble());
- }
-
- public static QuotaUsage create(double rate) {
- return new QuotaUsage(rate);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- QuotaUsage that = (QuotaUsage) o;
- return Double.compare(that.rate, rate) == 0;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(rate);
- }
-
- @Override
- public String toString() {
- return "QuotaUsage{" +
- "rate=" + rate +
- '}';
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java
deleted file mode 100644
index d3ab2216539..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/SystemApplication.java
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.applicationmodel.InfrastructureApplication;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ServiceConvergence;
-
-import java.util.Arrays;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * This represents a system-level application in hosted Vespa. All infrastructure nodes in a hosted Vespa zones are
- * allocated to a system application.
- *
- * @author mpolden
- */
-public enum SystemApplication {
-
- controllerHost(InfrastructureApplication.CONTROLLER_HOST),
- configServerHost(InfrastructureApplication.CONFIG_SERVER_HOST),
- configServer(InfrastructureApplication.CONFIG_SERVER),
- proxyHost(InfrastructureApplication.PROXY_HOST),
- proxy(InfrastructureApplication.PROXY, configServer),
- tenantHost(InfrastructureApplication.TENANT_HOST);
-
- /** The tenant owning all system applications */
- public static final TenantName TENANT = TenantName.from("hosted-vespa");
-
- private final InfrastructureApplication application;
- private final List<SystemApplication> dependencies;
-
- SystemApplication(InfrastructureApplication application, SystemApplication... dependencies) {
- this.application = application;
- this.dependencies = List.of(dependencies);
- }
-
- public ApplicationId id() {
- return application.id();
- }
-
- /** The node type that is implicitly allocated to this */
- public NodeType nodeType() {
- return application.nodeType();
- }
-
- /** Returns the system applications that should upgrade before this */
- public List<SystemApplication> dependencies() { return dependencies; }
-
- /** Returns whether this system application has an application package */
- public boolean hasApplicationPackage() {
- return this == proxy;
- }
-
- /** Returns whether config for this application has converged in given zone */
- public boolean configConvergedIn(ZoneId zone, Controller controller, Optional<Version> version) {
- if (!hasApplicationPackage()) {
- return true;
- }
- return controller.serviceRegistry().configServer().serviceConvergence(new DeploymentId(id(), zone), version)
- .map(ServiceConvergence::converged)
- .orElse(false);
- }
-
- /** Returns whether this should receive OS upgrades */
- public boolean shouldUpgradeOs() {
- return nodeType().isHost();
- }
-
- /** All system applications that are not the controller */
- public static List<SystemApplication> notController() {
- return List.copyOf(EnumSet.complementOf(EnumSet.of(SystemApplication.controllerHost)));
- }
-
- /** All system applications */
- public static List<SystemApplication> all() {
- return List.of(values());
- }
-
- /** Returns the system application matching given id, if any */
- public static Optional<SystemApplication> matching(ApplicationId id) {
- return Arrays.stream(values()).filter(app -> app.id().equals(id)).findFirst();
- }
-
- @Override
- public String toString() {
- return Text.format("system application %s of type %s", id(), nodeType());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java
deleted file mode 100644
index 9c9ec35fa80..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/TenantAndApplicationId.java
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.TenantName;
-
-import java.util.Objects;
-
-/**
- * Tenant and application name pair.
- *
- * @author jonmv
- */
-public class TenantAndApplicationId implements Comparable<TenantAndApplicationId> {
-
- private final TenantName tenant;
- private final ApplicationName application;
-
- private TenantAndApplicationId(TenantName tenant, ApplicationName application) {
- requireNonBlank(tenant.value(), "Tenant name");
- requireNonBlank(application.value(), "Application name");
- this.tenant = tenant;
- this.application = application;
- }
-
- public static TenantAndApplicationId from(TenantName tenant, ApplicationName application) {
- return new TenantAndApplicationId(tenant, application);
- }
-
- public static TenantAndApplicationId from(String tenant, String application) {
- return from(TenantName.from(tenant), ApplicationName.from(application));
- }
-
- public static TenantAndApplicationId fromSerialized(String value) {
- String[] parts = value.split(":");
- if (parts.length != 2)
- throw new IllegalArgumentException("Serialized value should be '<tenant>:<application>', but was '" + value + "'");
-
- return from(parts[0], parts[1]);
- }
-
- public static TenantAndApplicationId from(ApplicationId id) {
- return from(id.tenant(), id.application());
- }
-
- public ApplicationId defaultInstance() {
- return instance(InstanceName.defaultName());
- }
-
- public ApplicationId instance(String instance) {
- return instance(InstanceName.from(instance));
- }
-
- public ApplicationId instance(InstanceName instance) {
- return ApplicationId.from(tenant, application, instance);
- }
-
- public String serialized() {
- return tenant.value() + ":" + application.value();
- }
-
- public TenantName tenant() {
- return tenant;
- }
-
- public ApplicationName application() {
- return application;
- }
-
- @Override
- public boolean equals(Object other) {
- if (this == other) return true;
- if (other == null || getClass() != other.getClass()) return false;
- TenantAndApplicationId that = (TenantAndApplicationId) other;
- return tenant.equals(that.tenant) &&
- application.equals(that.application);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(tenant, application);
- }
-
- @Override
- public int compareTo(TenantAndApplicationId other) {
- int tenantComparison = tenant.compareTo(other.tenant);
- return tenantComparison != 0 ? tenantComparison : application.compareTo(other.application);
- }
-
- @Override
- public String toString() {
- return tenant.value() + "." + application.value();
- }
-
- private static void requireNonBlank(String value, String name) {
- Objects.requireNonNull(value, name + " cannot be null");
- if (name.isBlank())
- throw new IllegalArgumentException(name + " cannot be blank");
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/package-info.java
deleted file mode 100644
index 6a685281dbb..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/package-info.java
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * Core application model
- *
- * @author bratseth
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java
deleted file mode 100644
index 27e45aa1e7d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackage.java
+++ /dev/null
@@ -1,318 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import com.google.common.hash.Hasher;
-import com.google.common.hash.Hashing;
-import com.google.common.hash.HashingOutputStream;
-import com.yahoo.component.Version;
-import com.yahoo.config.application.FileSystemWrapper;
-import com.yahoo.config.application.FileSystemWrapper.FileWrapper;
-import com.yahoo.config.application.XmlPreProcessor;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.Tags;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.archive.ArchiveStreamReader;
-import com.yahoo.vespa.archive.ArchiveStreamReader.ArchiveFile;
-import com.yahoo.vespa.archive.ArchiveStreamReader.Options;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.deployment.ZipBuilder;
-import com.yahoo.yolean.Exceptions;
-import org.w3c.dom.Document;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.UncheckedIOException;
-import java.nio.file.NoSuchFileException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.ConcurrentSkipListMap;
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-import static com.yahoo.slime.Type.NIX;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.stream.Collectors.toMap;
-
-/**
- * A representation of the content of an application package.
- * Only meta-data content can be accessed as anything other than compressed data.
- * A package is identified by a hash of the content.
- *
- * @author bratseth
- * @author jonmv
- */
-public class ApplicationPackage {
-
- public static final String deploymentFile = "deployment.xml";
- static final String trustedCertificatesDir = "security/";
- static final String trustedCertificatesFile = trustedCertificatesDir + "clients.pem";
- static final String buildMetaFile = "build-meta.json";
- static final String validationOverridesFile = "validation-overrides.xml";
- static final String servicesFile = "services.xml";
- static final Set<String> prePopulated = Set.of(deploymentFile, validationOverridesFile, servicesFile, buildMetaFile, trustedCertificatesFile);
-
- private static Hasher hasher() { return Hashing.murmur3_128().newHasher(); }
-
- private final String bundleHash;
- private final byte[] zippedContent;
- private final DeploymentSpec deploymentSpec;
- private final ValidationOverrides validationOverrides;
- private final ZipArchiveCache files;
- private final Optional<Version> compileVersion;
- private final Optional<Instant> buildTime;
- private final Optional<Version> parentVersion;
-
- /**
- * Creates an application package from its zipped content.
- * This <b>assigns ownership</b> of the given byte array to this class;
- * it must not be further changed by the caller.
- */
- public ApplicationPackage(byte[] zippedContent) {
- this(zippedContent, false, false);
- }
-
- /**
- * Creates an application package from its zipped content.
- * This <b>assigns ownership</b> of the given byte array to this class;
- * it must not be further changed by the caller.
- * If 'requireFiles' is true, files needed by deployment orchestration must be present.
- */
- public ApplicationPackage(byte[] zippedContent, boolean requireFiles, boolean checkCertificateFile) {
- this.zippedContent = Objects.requireNonNull(zippedContent, "The application package content cannot be null");
- this.files = new ZipArchiveCache(zippedContent, prePopulated, checkCertificateFile);
-
- Optional<DeploymentSpec> deploymentSpec = files.get(deploymentFile).map(bytes -> new String(bytes, UTF_8)).map(DeploymentSpec::fromXml);
- if (requireFiles && deploymentSpec.isEmpty())
- throw new IllegalArgumentException("Missing required file '" + deploymentFile + "'");
- this.deploymentSpec = deploymentSpec.orElse(DeploymentSpec.empty);
-
- this.validationOverrides = files.get(validationOverridesFile).map(bytes -> new String(bytes, UTF_8)).map(ValidationOverrides::fromXml).orElse(ValidationOverrides.empty);
-
- Optional<Inspector> buildMetaObject = files.get(buildMetaFile).map(SlimeUtils::jsonToSlime).map(Slime::get);
- this.compileVersion = buildMetaObject.flatMap(object -> parse(object, "compileVersion", field -> Version.fromString(field.asString())));
- this.buildTime = buildMetaObject.flatMap(object -> parse(object, "buildTime", field -> Instant.ofEpochMilli(field.asLong())));
- this.parentVersion = buildMetaObject.flatMap(object -> parse(object, "parentVersion", field -> Version.fromString(field.asString())));
-
- this.bundleHash = calculateBundleHash(zippedContent);
-
- preProcessAndPopulateCache();
- }
-
- /** Hash of all files and settings that influence what is deployed to config servers. */
- public String bundleHash() {
- return bundleHash;
- }
-
- /** Returns the content of this package. The content <b>must not</b> be modified. */
- public byte[] zippedContent() { return zippedContent; }
-
- /**
- * Returns the deployment spec from the deployment.xml file of the package content.<br>
- * This is the DeploymentSpec.empty instance if this package does not contain a deployment.xml file.<br>
- * <em>NB: <strong>Always</strong> read deployment spec from the {@link Application}, for deployment orchestration.</em>
- */
- public DeploymentSpec deploymentSpec() { return deploymentSpec; }
-
- /**
- * Returns the validation overrides from the validation-overrides.xml file of the package content.
- * This is the ValidationOverrides.empty instance if this package does not contain a validation-overrides.xml file.
- */
- public ValidationOverrides validationOverrides() { return validationOverrides; }
-
- /** Returns a basic variant of services.xml contained in this package, pre-processed according to given deployment and tags */
- public BasicServicesXml services(DeploymentId deployment, Tags tags) {
- FileWrapper servicesXml = files.wrapper().wrap(Paths.get(servicesFile));
- if (!servicesXml.exists()) return BasicServicesXml.empty;
- try {
- Document document = new XmlPreProcessor(files.wrapper().wrap(Paths.get("./")),
- new InputStreamReader(new ByteArrayInputStream(servicesXml.content()), UTF_8),
- deployment.applicationId().instance(),
- deployment.zoneId().environment(),
- deployment.zoneId().region(),
- tags).run();
- return BasicServicesXml.parse(document);
- } catch (IllegalArgumentException e) {
- throw e;
- } catch (Exception e) {
- throw new IllegalArgumentException(e);
- }
- }
-
- /** Returns the platform version which package was compiled against, if known. */
- public Optional<Version> compileVersion() { return compileVersion; }
-
- /** Returns the time this package was built, if known. */
- public Optional<Instant> buildTime() { return buildTime; }
-
- /** Returns the parent version used to compile the package, if known. */
- public Optional<Version> parentVersion() { return parentVersion; }
-
- private static <Type> Optional<Type> parse(Inspector buildMetaObject, String fieldName, Function<Inspector, Type> mapper) {
- Inspector field = buildMetaObject.field(fieldName);
- if ( ! field.valid() || field.type() == NIX)
- return Optional.empty();
- try {
- return Optional.of(mapper.apply(buildMetaObject.field(fieldName)));
- }
- catch (RuntimeException e) {
- throw new IllegalArgumentException("Failed parsing \"" + fieldName + "\" in '" + buildMetaFile + "': " + Exceptions.toMessageString(e));
- }
- }
-
- /** Creates a valid application package that will remove all application's deployments */
- public static ApplicationPackage deploymentRemoval() {
- return new ApplicationPackage(filesZip(Map.of(validationOverridesFile, allValidationOverrides().xmlForm().getBytes(UTF_8),
- deploymentFile, DeploymentSpec.empty.xmlForm().getBytes(UTF_8))));
- }
-
- /** Returns a zip containing metadata about deployments of this package by the given job. */
- public byte[] metaDataZip() {
- return cacheZip();
- }
-
- private void preProcessAndPopulateCache() {
- FileWrapper servicesXml = files.wrapper().wrap(Paths.get(servicesFile));
- if (servicesXml.exists())
- try {
- new XmlPreProcessor(files.wrapper().wrap(Paths.get("./")),
- new InputStreamReader(new ByteArrayInputStream(servicesXml.content()), UTF_8),
- InstanceName.defaultName(),
- Environment.prod,
- RegionName.defaultName(),
- Tags.empty())
- .run(); // Populates the zip archive cache with files that would be included.
- }
- catch (IllegalArgumentException e) {
- throw e;
- }
- catch (Exception e) {
- throw new IllegalArgumentException(e);
- }
- }
-
- private byte[] cacheZip() {
- return filesZip(files.cache.entrySet().stream()
- .filter(entry -> entry.getValue().isPresent())
- .collect(toMap(entry -> entry.getKey().toString(),
- entry -> entry.getValue().get())));
- }
-
- public static byte[] filesZip(Map<String, byte[]> files) {
- try (ZipBuilder zipBuilder = new ZipBuilder(files.values().stream().mapToInt(bytes -> bytes.length).sum() + 512)) {
- files.forEach(zipBuilder::add);
- zipBuilder.close();
- return zipBuilder.toByteArray();
- }
- }
-
- private static ValidationOverrides allValidationOverrides() {
- String until = DateTimeFormatter.ISO_LOCAL_DATE.format(Instant.now().plus(Duration.ofDays(25)).atZone(ZoneOffset.UTC));
- StringBuilder validationOverridesContents = new StringBuilder(1000);
- validationOverridesContents.append("<validation-overrides version=\"1.0\">\n");
- for (ValidationId validationId: ValidationId.values())
- validationOverridesContents.append("\t<allow until=\"").append(until).append("\">").append(validationId.value()).append("</allow>\n");
- validationOverridesContents.append("</validation-overrides>\n");
-
- return ValidationOverrides.fromXml(validationOverridesContents.toString());
- }
-
- // Hashes all files and settings that require a deployment to be forwarded to configservers
- private String calculateBundleHash(byte[] zippedContent) {
- Predicate<String> entryMatcher = name -> ! name.endsWith(deploymentFile) && ! name.endsWith(buildMetaFile);
- Options options = Options.standard().pathPredicate(entryMatcher);
- HashingOutputStream hashOut = new HashingOutputStream(Hashing.murmur3_128(-1), OutputStream.nullOutputStream());
- ArchiveFile file;
- try (ArchiveStreamReader reader = ArchiveStreamReader.ofZip(new ByteArrayInputStream(zippedContent), options)) {
- while ((file = reader.readNextTo(hashOut)) != null) {
- hashOut.write(file.path().toString().getBytes(UTF_8));
- }
- }
- catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- return hasher().putLong(hashOut.hash().asLong())
- .putInt(deploymentSpec.deployableHashCode())
- .hash().toString();
- }
-
- public static String calculateHash(byte[] bytes) {
- return hasher().putBytes(bytes)
- .hash().toString();
- }
-
-
- /** Maps normalized paths to cached content read from a zip archive. */
- private static class ZipArchiveCache {
-
- /** Max size of each extracted file */
- private static final int maxSize = 10 << 20; // 10 Mb
-
- private final byte[] zip;
- private final Map<Path, Optional<byte[]>> cache;
-
- public ZipArchiveCache(byte[] zip, Collection<String> prePopulated, boolean checkCertificateFile) {
- this.zip = zip;
- this.cache = new ConcurrentSkipListMap<>();
- this.cache.putAll(read(prePopulated));
- if (checkCertificateFile)
- verifyThatTrustedCertificateExists();
- }
-
- public Optional<byte[]> get(String path) {
- return get(Paths.get(path));
- }
-
- public Optional<byte[]> get(Path path) {
- return cache.computeIfAbsent(path.normalize(), read(List.of(path.normalize().toString()))::get);
- }
-
- public FileSystemWrapper wrapper() {
- return FileSystemWrapper.ofFiles(Path.of("./"), // zip archive root
- path -> get(path).isPresent(), // Assume content asked for will also be read ...
- path -> get(path).orElseThrow(() -> new NoSuchFileException(path.toString())));
- }
-
- private Map<Path, Optional<byte[]>> read(Collection<String> names) {
- var entries = findZipFileEntries(names::contains);
- names.stream().map(Paths::get).forEach(path -> entries.putIfAbsent(path.normalize(), Optional.empty()));
- return entries;
- }
-
-
- private void verifyThatTrustedCertificateExists() {
- // Any name is valid for certificate files
- var entries = findZipFileEntries((entry) -> entry.contains(trustedCertificatesDir) && entry.endsWith(".pem"));
- if (entries.size() == 0)
- throw new IllegalArgumentException("No client certificate found in " + trustedCertificatesDir + " in application package" +
- ", see https://cloud.vespa.ai/en/security/guide");
- }
-
- private Map<Path, Optional<byte[]>> findZipFileEntries(Predicate<String> names) {
- return ZipEntries.from(zip, names, maxSize, true)
- .asList().stream()
- .collect(toMap(entry -> Paths.get(entry.name()).normalize(),
- ZipEntries.ZipEntryWithContent::content));
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageDiff.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageDiff.java
deleted file mode 100644
index bd08def6cec..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageDiff.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.UncheckedIOException;
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.vespa.hosted.controller.application.pkg.ZipEntries.ZipEntryWithContent;
-
-/**
- * @author freva
- */
-public class ApplicationPackageDiff {
-
- public static byte[] diffAgainstEmpty(ApplicationPackage right) {
- byte[] emptyZip = new byte[]{80, 75, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
- return diff(new ApplicationPackage(emptyZip), right);
- }
-
- public static byte[] diff(ApplicationPackage left, ApplicationPackage right) {
- return diff(left, right, 10 << 20, 1 << 20, 10 << 20);
- }
-
- static byte[] diff(ApplicationPackage left, ApplicationPackage right, int maxFileSizeToDiff, int maxDiffSizePerFile, int maxTotalDiffSize) {
- if (Arrays.equals(left.zippedContent(), right.zippedContent())) return "No diff\n".getBytes(StandardCharsets.UTF_8);
-
- Map<String, ZipEntryWithContent> leftContents = readContents(left, maxFileSizeToDiff);
- Map<String, ZipEntryWithContent> rightContents = readContents(right, maxFileSizeToDiff);
-
- StringBuilder sb = new StringBuilder();
- List<String> files = Stream.of(leftContents, rightContents)
- .flatMap(contents -> contents.keySet().stream())
- .sorted()
- .distinct()
- .toList();
- for (String file : files) {
- if (sb.length() > maxTotalDiffSize)
- sb.append("--- ").append(file).append('\n').append("Diff skipped: Total diff size >").append(maxTotalDiffSize).append("B)\n\n");
- else
- diff(Optional.ofNullable(leftContents.get(file)), Optional.ofNullable(rightContents.get(file)), maxDiffSizePerFile)
- .ifPresent(diff -> sb.append("--- ").append(file).append('\n').append(diff).append('\n'));
- }
-
- return (sb.length() == 0 ? "No diff\n" : sb.toString()).getBytes(StandardCharsets.UTF_8);
- }
-
- private static Optional<String> diff(Optional<ZipEntryWithContent> left, Optional<ZipEntryWithContent> right, int maxDiffSizePerFile) {
- Optional<byte[]> leftContent = left.flatMap(ZipEntryWithContent::content);
- Optional<byte[]> rightContent = right.flatMap(ZipEntryWithContent::content);
- if (leftContent.isPresent() && rightContent.isPresent() && Arrays.equals(leftContent.get(), rightContent.get()))
- return Optional.empty();
-
- if (Stream.of(left, right).flatMap(Optional::stream).anyMatch(entry -> entry.content().isEmpty()))
- return Optional.of(String.format("Diff skipped: File too large (%s -> %s)\n",
- left.map(e -> e.size() + "B").orElse("new file"), right.map(e -> e.size() + "B").orElse("file deleted")));
-
- if (Stream.of(leftContent, rightContent).flatMap(Optional::stream).anyMatch(c -> isBinary(c)))
- return Optional.of(String.format("Diff skipped: File is binary (%s -> %s)\n",
- left.map(e -> e.size() + "B").orElse("new file"), right.map(e -> e.size() + "B").orElse("file deleted")));
-
- return LinesComparator.diff(
- leftContent.map(c -> lines(c)).orElseGet(List::of),
- rightContent.map(c -> lines(c)).orElseGet(List::of))
- .map(diff -> diff.length() > maxDiffSizePerFile ? "Diff skipped: Diff too large (" + diff.length() + "B)\n" : diff);
- }
-
- private static Map<String, ZipEntryWithContent> readContents(ApplicationPackage app, int maxFileSizeToDiff) {
- return ZipEntries.from(app.zippedContent(), entry -> true, maxFileSizeToDiff, false).asList().stream()
- .collect(Collectors.toMap(ZipEntryWithContent::name, e -> e));
- }
-
- private static List<String> lines(byte[] data) {
- List<String> lines = new ArrayList<>(Math.min(16, data.length / 100));
- try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(data), StandardCharsets.UTF_8))) {
- String line;
- while ((line = bufferedReader.readLine()) != null) {
- lines.add(line);
- }
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- return lines;
- }
-
- private static boolean isBinary(byte[] data) {
- if (data.length == 0) return false;
-
- int lengthToCheck = Math.min(data.length, 10000);
- int ascii = 0;
-
- for (int i = 0; i < lengthToCheck; i++) {
- byte b = data[i];
- if (b < 0x9) return true;
-
- // TAB, newline/line feed, carriage return
- if (b == 0x9 || b == 0xA || b == 0xD) ascii++;
- else if (b >= 0x20 && b <= 0x7E) ascii++;
- }
-
- return (double) ascii / lengthToCheck < 0.95;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageStream.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageStream.java
deleted file mode 100644
index e13dd2acbdb..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageStream.java
+++ /dev/null
@@ -1,246 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import java.io.ByteArrayOutputStream;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.nio.file.attribute.FileTime;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-import java.util.function.UnaryOperator;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-import java.util.zip.ZipOutputStream;
-
-import static java.io.OutputStream.nullOutputStream;
-import static java.lang.Math.min;
-
-/**
- * Wraps a zipped application package stream.
- * This allows replacing content as the input stream is read.
- * This also retains a truncated {@link ApplicationPackage}, containing only the specified set of files,
- * which can be accessed when this stream is fully exhausted.
- *
- * @author jonmv
- */
-public class ApplicationPackageStream {
-
- private final Supplier<Replacer> replacer;
- private final Supplier<Predicate<String>> filter;
- private final Supplier<InputStream> in;
- private final AtomicReference<ApplicationPackage> truncatedPackage = new AtomicReference<>();
- private final FileTime createdAt = FileTime.fromMillis(System.currentTimeMillis());
-
- /** Stream that copies application meta and other XML files from the input stream to its {@link #truncatedPackage()} when exhausted. */
- public ApplicationPackageStream(Supplier<InputStream> in) {
- this(in, () -> name -> ApplicationPackage.prePopulated.contains(name) || name.endsWith(".xml"), Map.of());
- }
-
- /** Stream that copies the indicated entries from the input stream to its {@link #truncatedPackage()} when exhausted. */
- public ApplicationPackageStream(Supplier<InputStream> in, Supplier<Predicate<String>> truncation) {
- this(in, truncation, Map.of());
- }
-
- /** Stream that replaces the indicated entries, and copies the filtered entries to its {@link #truncatedPackage()} when exhausted. */
- public ApplicationPackageStream(Supplier<InputStream> in, Supplier<Predicate<String>> truncation, Map<String, UnaryOperator<InputStream>> replacements) {
- this(in, truncation, Replacer.of(replacements));
- }
-
- /** Stream that uses the given replacer to modify content, and copies the filtered entries to its {@link #truncatedPackage()} when exhausted. */
- public ApplicationPackageStream(Supplier<InputStream> in, Supplier<Predicate<String>> truncation, Supplier<Replacer> replacer) {
- this.in = in;
- this.filter = truncation;
- this.replacer = replacer;
- }
-
- /**
- * Returns a new stream containing the zipped application package this wraps. Separate streams may exist concurrently,
- * and the first to be exhausted will populate the truncated application package.
- */
- public InputStream zipStream() {
- return new Stream(in.get(), replacer.get(), filter.get(), createdAt, truncatedPackage);
- }
-
- /**
- * Returns the application package backed by only the files indicated by the truncation filter.
- * Throws if no instances of {@link #zipStream()} have been exhausted yet.
- */
- public ApplicationPackage truncatedPackage() {
- ApplicationPackage truncated = truncatedPackage.get();
- if (truncated == null) throw new IllegalStateException("must completely exhaust input before reading package");
- return truncated;
- }
-
- private static class Stream extends InputStream {
-
- private final byte[] inBuffer = new byte[1 << 16];
- private final ByteArrayOutputStream teeOut = new ByteArrayOutputStream(1 << 16);
- private final ZipOutputStream teeZip = new ZipOutputStream(teeOut);
- private final ByteArrayOutputStream out = new ByteArrayOutputStream(1 << 16);
- private final ZipOutputStream outZip = new ZipOutputStream(out);
- private final AtomicReference<ApplicationPackage> truncatedPackage;
- private final InputStream in;
- private final ZipInputStream inZip;
- private final Replacer replacer;
- private final Predicate<String> filter;
- private final FileTime createdAt;
- private byte[] currentOut = new byte[0];
- private InputStream currentIn = InputStream.nullInputStream();
- private boolean includeCurrent = false;
- private int pos = 0;
- private boolean closed = false;
- private boolean done = false;
-
- private Stream(InputStream in, Replacer replacer, Predicate<String> filter, FileTime createdAt, AtomicReference<ApplicationPackage> truncatedPackage) {
- this.in = in;
- this.inZip = new ZipInputStream(in);
- this.replacer = replacer;
- this.filter = filter;
- this.createdAt = createdAt;
- this.truncatedPackage = truncatedPackage;
- }
-
- private void fill() throws IOException {
- if (done) return;
- while (out.size() == 0) {
- // Exhaust current entry first.
- int i, n = out.size();
- while (out.size() == 0 && (i = currentIn.read(inBuffer)) != -1) {
- if (includeCurrent) teeZip.write(inBuffer, 0, i);
- outZip.write(inBuffer, 0, i);
- n += i;
- }
-
- // Current entry exhausted, look for next.
- if (n == 0) {
- next();
- if (done) break;
- }
- }
-
- currentOut = out.toByteArray();
- out.reset();
- pos = 0;
- }
-
- private void next() throws IOException {
- if (includeCurrent) teeZip.closeEntry();
- outZip.closeEntry();
-
- ZipEntry next = inZip.getNextEntry();
- String name;
- FileTime modifiedAt;
- InputStream content = null;
- if (next == null) {
- // We may still have replacements to fill in, but if we don't, we're done filling, forever!
- name = replacer.next();
- modifiedAt = createdAt;
- if (name == null) {
- outZip.close(); // This typically makes new output available, so must check for that after this.
- teeZip.close();
- currentIn = nullInputStream();
- truncatedPackage.compareAndSet(null, new ApplicationPackage(teeOut.toByteArray()));
- done = true;
- return;
- }
- }
- else {
- name = next.getName();
- modifiedAt = next.getLastModifiedTime();
- content = new FilterInputStream(inZip) { @Override public void close() { } }; // Protect inZip from replacements closing it.
- }
-
- includeCurrent = truncatedPackage.get() == null && filter.test(name);
- currentIn = replacer.modify(name, content);
- if (currentIn == null) {
- currentIn = InputStream.nullInputStream();
- }
- else {
- if (includeCurrent) teeZip.putNextEntry(new ZipEntry(name) {{ setLastModifiedTime(modifiedAt); }});
- outZip.putNextEntry(new ZipEntry(name) {{ setLastModifiedTime(modifiedAt); }});
- }
- }
-
- @Override
- public int read() throws IOException {
- if (closed) throw new IOException("stream closed");
- if (pos == currentOut.length) {
- fill();
- if (pos == currentOut.length) return -1;
- }
- return 0xff & currentOut[pos++];
- }
-
- @Override
- public int read(byte[] out, int off, int len) throws IOException {
- if (closed) throw new IOException("stream closed");
- if ((off | len | (off + len) | (out.length - (off + len))) < 0) throw new IndexOutOfBoundsException();
- if (pos == currentOut.length) {
- fill();
- if (pos == currentOut.length) return -1;
- }
- int n = min(currentOut.length - pos, len);
- System.arraycopy(currentOut, pos, out, off, n);
- pos += n;
- return n;
- }
-
- @Override
- public int available() throws IOException {
- return pos == currentOut.length && done ? 0 : 1;
- }
-
- @Override
- public void close() {
- if ( ! closed) try {
- transferTo(nullOutputStream()); // Finish reading the zip, to populate the truncated package in case of errors.
- in.transferTo(nullOutputStream()); // For some inane reason, ZipInputStream doesn't exhaust its wrapped input.
- inZip.close();
- closed = true;
- }
- catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- }
-
- /** Replaces entries in a zip stream as they are encountered, then appends remaining entries at the end. */
- public interface Replacer {
-
- /** Called when the entries of the original zip stream are exhausted. Return remaining names, or {@code null} when none left. */
- String next();
-
- /** Modify content for a given name; return {@code null} for removal; in is {@code null} for entries not present in the input. */
- InputStream modify(String name, InputStream in);
-
- /**
- * Wraps a map of fixed replacements, and:
- * <ul>
- * <li>Removes entries whose value is {@code null}.</li>
- * <li>Modifies entries present in both input and the map.</li>
- * <li>Appends entries present exclusively in the map.</li>
- * <li>Writes all other entries as they are.</li>
- * </ul>
- */
- static Supplier<Replacer> of(Map<String, UnaryOperator<InputStream>> replacements) {
- return () -> new Replacer() {
- final Map<String, UnaryOperator<InputStream>> remaining = new HashMap<>(replacements);
- @Override public String next() {
- return remaining.isEmpty() ? null : remaining.keySet().iterator().next();
- }
- @Override public InputStream modify(String name, InputStream in) {
- UnaryOperator<InputStream> mapper = remaining.remove(name);
- return mapper == null ? in : mapper.apply(in);
- }
- };
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java
deleted file mode 100644
index 5412fdf03a3..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageValidator.java
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.DeploymentSpec.DeclaredZone;
-import com.yahoo.config.application.api.Endpoint;
-import com.yahoo.config.application.api.Endpoint.Level;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.ZoneEndpoint;
-import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
-
-import static java.util.stream.Collectors.joining;
-
-/**
- * This contains validators for a {@link ApplicationPackage} that depend on a {@link Controller} to perform validation.
- *
- * @author mpolden
- */
-public class ApplicationPackageValidator {
-
- private final Controller controller;
-
- public ApplicationPackageValidator(Controller controller) {
- this.controller = Objects.requireNonNull(controller, "controller must be non-null");
- }
-
- /**
- * Validate the given application package
- *
- * @throws IllegalArgumentException if any validations fail
- */
- public void validate(Application application, ApplicationPackage applicationPackage, Instant instant) {
- validateSteps(applicationPackage.deploymentSpec());
- validateEndpointRegions(applicationPackage.deploymentSpec());
- validateEndpointChange(application, applicationPackage, instant);
- validateCompactedEndpoint(applicationPackage);
- validateDeprecatedElements(applicationPackage);
- validateCloudAccounts(application, applicationPackage);
- }
-
- private void validateCloudAccounts(Application application, ApplicationPackage applicationPackage) {
- Set<CloudAccount> tenantAccounts = new TreeSet<>(controller.applications().accountsOf(application.id().tenant()));
- Set<CloudAccount> declaredAccounts = new TreeSet<>(applicationPackage.deploymentSpec().cloudAccounts().values());
- for (DeploymentInstanceSpec instance : applicationPackage.deploymentSpec().instances())
- for (ZoneId zone : controller.zoneRegistry().zones().controllerUpgraded().ids())
- declaredAccounts.addAll(instance.cloudAccounts(zone.environment(), zone.region()).values());
-
- declaredAccounts.removeIf(tenantAccounts::contains);
- declaredAccounts.removeIf(CloudAccount::isUnspecified);
- if ( ! declaredAccounts.isEmpty())
- throw new IllegalArgumentException("cloud accounts " +
- declaredAccounts.stream().map(CloudAccount::value).collect(joining(", ", "[", "]")) +
- " are not valid for tenant " +
- application.id().tenant());
- }
-
- /** Verify that deployment spec does not use elements deprecated on a major version older than wanted major version */
- private void validateDeprecatedElements(ApplicationPackage applicationPackage) {
- int wantedMajor = applicationPackage.compileVersion().map(Version::getMajor)
- .or(() -> applicationPackage.deploymentSpec().majorVersion())
- .orElseGet(() -> controller.readSystemVersion().getMajor());
- for (var deprecatedElement : applicationPackage.deploymentSpec().deprecatedElements()) {
- if (deprecatedElement.majorVersion() >= wantedMajor) continue;
- throw new IllegalArgumentException(deprecatedElement.humanReadableString());
- }
- }
-
- /** Verify that each of the production zones listed in the deployment spec exist in this system */
- private void validateSteps(DeploymentSpec deploymentSpec) {
- for (var spec : deploymentSpec.instances()) {
- for (var zone : spec.zones()) {
- Environment environment = zone.environment();
- if (zone.region().isEmpty()) continue;
- ZoneId zoneId = ZoneId.from(environment, zone.region().get());
- if (!controller.zoneRegistry().hasZone(zoneId)) {
- throw new IllegalArgumentException("Zone " + zone + " in deployment spec was not found in this system!");
- }
- }
- }
- }
-
- /** Verify that:
- * <ul>
- * <li>no single endpoint contains regions in different clouds</li>
- * <li>application endpoints with different regions must be contained in CGP and AWS</li>
- * </ul>
- */
- private void validateEndpointRegions(DeploymentSpec deploymentSpec) {
- for (var instance : deploymentSpec.instances()) {
- validateEndpointRegions(instance.endpoints(), instance);
- }
- validateEndpointRegions(deploymentSpec.endpoints(), null);
- }
-
- private void validateEndpointRegions(List<Endpoint> endpoints, DeploymentInstanceSpec instance) {
- for (var endpoint : endpoints) {
- RegionName[] regions = new HashSet<>(endpoint.regions()).toArray(RegionName[]::new);
- Set<CloudName> clouds = controller.zoneRegistry().zones().all().in(Environment.prod)
- .in(regions)
- .zones().stream()
- .map(ZoneApi::getCloudName)
- .collect(Collectors.toSet());
- String endpointString = instance == null ? "Application endpoint '" + endpoint.endpointId() + "'"
- : "Endpoint '" + endpoint.endpointId() + "' in " + instance;
- if (Set.of(CloudName.GCP, CloudName.AWS).containsAll(clouds)) { } // Everything is fine!
- else if (Set.of(CloudName.YAHOO).containsAll(clouds) || Set.of(CloudName.DEFAULT).containsAll(clouds)) {
- if (endpoint.level() == Level.application && regions.length != 1) {
- throw new IllegalArgumentException(endpointString + " cannot contain different regions: " +
- endpoint.regions().stream().sorted().toList());
- }
- }
- else if (clouds.size() == 1) {
- throw new IllegalArgumentException("unknown cloud '" + clouds.iterator().next() + "'");
- }
- else {
- throw new IllegalArgumentException(endpointString + " cannot contain regions in different clouds: " +
- endpoint.regions().stream().sorted().toList());
- }
- }
- }
-
- /** Verify endpoint configuration of given application package */
- private void validateEndpointChange(Application application, ApplicationPackage applicationPackage, Instant instant) {
- for (DeploymentInstanceSpec instance : applicationPackage.deploymentSpec().instances()) {
- validateGlobalEndpointChanges(application, instance.name(), applicationPackage, instant);
- validateZoneEndpointChanges(application, instance.name(), applicationPackage, instant);
- }
- }
-
- /** Verify that compactable endpoint parts (instance name and endpoint ID) do not clash */
- private void validateCompactedEndpoint(ApplicationPackage applicationPackage) {
- Map<List<String>, InstanceEndpoint> instanceEndpoints = new HashMap<>();
- for (var instanceSpec : applicationPackage.deploymentSpec().instances()) {
- for (var endpoint : instanceSpec.endpoints()) {
- List<String> nonCompactableIds = nonCompactableIds(instanceSpec.name(), endpoint);
- InstanceEndpoint instanceEndpoint = new InstanceEndpoint(instanceSpec.name(), endpoint.endpointId());
- InstanceEndpoint existingEndpoint = instanceEndpoints.get(nonCompactableIds);
- if (existingEndpoint != null) {
- throw new IllegalArgumentException("Endpoint with ID '" + endpoint.endpointId() + "' in instance '"
- + instanceSpec.name().value() +
- "' clashes with endpoint '" + existingEndpoint.endpointId +
- "' in instance '" + existingEndpoint.instance + "'");
- }
- instanceEndpoints.put(nonCompactableIds, instanceEndpoint);
- }
- }
- }
-
- /** Verify changes to endpoint configuration by comparing given application package to the existing one, if any */
- private void validateGlobalEndpointChanges(Application application, InstanceName instanceName, ApplicationPackage applicationPackage, Instant instant) {
- var validationId = ValidationId.globalEndpointChange;
- if (applicationPackage.validationOverrides().allows(validationId, instant)) return;
-
- var endpoints = application.deploymentSpec().instance(instanceName)
- .map(deploymentInstanceSpec1 -> deploymentInstanceSpec1.endpoints())
- .orElseGet(List::of);
- DeploymentInstanceSpec deploymentInstanceSpec = applicationPackage.deploymentSpec().requireInstance(instanceName);
- var newEndpoints = new ArrayList<>(deploymentInstanceSpec.endpoints());
-
- if (newEndpoints.containsAll(endpoints)) return; // Adding new endpoints is fine
- if (containsAllDestinationsOf(endpoints, newEndpoints)) return; // Adding destinations is fine
-
- var removedEndpoints = new ArrayList<>(endpoints);
- removedEndpoints.removeAll(newEndpoints);
- newEndpoints.removeAll(endpoints);
- throw new IllegalArgumentException(validationId.value() + ": application '" + application.id() +
- (instanceName.isDefault() ? "" : "." + instanceName.value()) +
- "' has endpoints " + endpoints +
- ", but does not include all of these in deployment.xml. Deploying given " +
- "deployment.xml will remove " + removedEndpoints +
- (newEndpoints.isEmpty() ? "" : " and add " + newEndpoints) +
- ". " + ValidationOverrides.toAllowMessage(validationId));
- }
-
- /** Verify changes to endpoint configuration by comparing given application package to the existing one, if any */
- private void validateZoneEndpointChanges(Application application, InstanceName instance, ApplicationPackage applicationPackage, Instant now) {
- ValidationId validationId = ValidationId.zoneEndpointChange;
- if (applicationPackage.validationOverrides().allows(validationId, now)) return;;
-
- String prefix = validationId + ": application '" + application.id() +
- (instance.isDefault() ? "" : "." + instance.value()) + "' ";
- DeploymentInstanceSpec spec = applicationPackage.deploymentSpec().requireInstance(instance);
- for (DeclaredZone zone : spec.zones()) {
- if (zone.environment() == Environment.prod) {
- Map<ClusterSpec.Id, ZoneEndpoint> newEndpoints = spec.zoneEndpoints(ZoneId.from(zone.environment(), zone.region().get()));
- application.deploymentSpec().instance(instance) // If old spec has this instance ...
- .filter(oldSpec -> oldSpec.concerns(zone.environment(), zone.region())) // ... and deploys to this zone ...
- .map(oldSpec -> oldSpec.zoneEndpoints(ZoneId.from(zone.environment(), zone.region().get())))
- .ifPresent(oldEndpoints -> { // ... then we compare the endpoints present in both.
- oldEndpoints.forEach((cluster, oldEndpoint) -> {
- ZoneEndpoint newEndpoint = newEndpoints.getOrDefault(cluster, ZoneEndpoint.defaultEndpoint);
- if ( ! newEndpoint.allowedUrns().containsAll(oldEndpoint.allowedUrns()))
- throw new IllegalArgumentException(prefix + "allows access to cluster '" + cluster.value() +
- "' in '" + zone.region().get().value() + "' to " +
- oldEndpoint.allowedUrns().stream().map(AllowedUrn::toString).collect(joining(", ", "[", "]")) +
- ", but does not include all these in the new deployment spec. " +
- "Deploying with the new settings will allow access to " +
- (newEndpoint.allowedUrns().isEmpty() ? "no one" : newEndpoint.allowedUrns().stream().map(AllowedUrn::toString).collect(joining(", ", "[", "]")) +
- ". " + ValidationOverrides.toAllowMessage(validationId)));
- });
- newEndpoints.forEach((cluster, newEndpoint) -> {
- ZoneEndpoint oldEndpoint = oldEndpoints.getOrDefault(cluster, ZoneEndpoint.defaultEndpoint);
- if (oldEndpoint.isPublicEndpoint() && ! newEndpoint.isPublicEndpoint())
- throw new IllegalArgumentException(prefix + "has a public endpoint for cluster '" + cluster.value() +
- "' in '" + zone.region().get().value() + "', but the new deployment spec " +
- "disables this. " + ValidationOverrides.toAllowMessage(validationId));
- });
- });
- }
- }
- }
-
- /** Returns whether newEndpoints contains all destinations in endpoints */
- private static boolean containsAllDestinationsOf(List<Endpoint> endpoints, List<Endpoint> newEndpoints) {
- var containsAllRegions = true;
- var hasSameCluster = true;
- for (var endpoint : endpoints) {
- var endpointContainsAllRegions = false;
- var endpointHasSameCluster = false;
- for (var newEndpoint : newEndpoints) {
- if (endpoint.endpointId().equals(newEndpoint.endpointId())) {
- endpointContainsAllRegions = newEndpoint.regions().containsAll(endpoint.regions());
- endpointHasSameCluster = newEndpoint.containerId().equals(endpoint.containerId());
- }
- }
- containsAllRegions &= endpointContainsAllRegions;
- hasSameCluster &= endpointHasSameCluster;
- }
- return containsAllRegions && hasSameCluster;
- }
-
- /** Returns a list of the non-compactable IDs of given instance and endpoint */
- private static List<String> nonCompactableIds(InstanceName instance, Endpoint endpoint) {
- List<String> ids = new ArrayList<>(2);
- if (!instance.isDefault()) {
- ids.add(instance.value());
- }
- if (!"default".equals(endpoint.endpointId())) {
- ids.add(endpoint.endpointId());
- }
- return ids;
- }
-
- private record InstanceEndpoint(InstanceName instance, String endpointId) {}
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/BasicServicesXml.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/BasicServicesXml.java
deleted file mode 100644
index da08ce108e3..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/BasicServicesXml.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import com.yahoo.text.XML;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.application.pkg.BasicServicesXml.Container.AuthMethod;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * A partially parsed variant of services.xml, for use by the {@link com.yahoo.vespa.hosted.controller.Controller}.
- *
- * @author mpolden
- */
-public record BasicServicesXml(List<Container> containers) {
-
- public static final BasicServicesXml empty = new BasicServicesXml(List.of());
-
- private static final String SERVICES_TAG = "services";
- private static final String CONTAINER_TAG = "container";
- private static final String CLIENTS_TAG = "clients";
- private static final String CLIENT_TAG = "client";
- private static final String TOKEN_TAG = "token";
-
- public BasicServicesXml(List<Container> containers) {
- this.containers = List.copyOf(Objects.requireNonNull(containers));
- }
-
- /** Parse a services.xml from given document */
- public static BasicServicesXml parse(Document document) {
- Element root = document.getDocumentElement();
- if (!root.getTagName().equals("services")) {
- throw new IllegalArgumentException("Root tag must be <" + SERVICES_TAG + ">");
- }
- List<BasicServicesXml.Container> containers = new ArrayList<>();
- for (var childNode : XML.getChildren(root)) {
- if (childNode.getTagName().equals(CONTAINER_TAG)) {
- String id = childNode.getAttribute("id");
- if (id.isEmpty()) {
- id = CONTAINER_TAG; // ID defaults to tag name when unset. See ConfigModelBuilder::getIdString
- }
- List<Container.AuthMethod> methods = new ArrayList<>();
- List<TokenId> tokens = new ArrayList<>();
- parseAuthMethods(childNode, methods, tokens);
- containers.add(new Container(id, methods, tokens));
- }
- }
- return new BasicServicesXml(containers);
- }
-
- private static void parseAuthMethods(Element containerNode, List<AuthMethod> methods, List<TokenId> tokens) {
- for (var node : XML.getChildren(containerNode)) {
- if (node.getTagName().equals(CLIENTS_TAG)) {
- for (var clientNode : XML.getChildren(node)) {
- if (clientNode.getTagName().equals(CLIENT_TAG)) {
- boolean tokenEnabled = false;
- for (var child : XML.getChildren(clientNode)) {
- if (TOKEN_TAG.equals(child.getTagName())) {
- tokenEnabled = true;
- tokens.add(TokenId.of(child.getAttribute("id")));
- }
- }
- methods.add(tokenEnabled ? Container.AuthMethod.token : Container.AuthMethod.mtls);
- }
- }
- }
- }
- if (methods.isEmpty()) {
- methods.add(Container.AuthMethod.mtls);
- }
- }
-
- /**
- * A Vespa container service.
- *
- * @param id ID of container
- * @param authMethods Authentication methods supported by this container
- */
- public record Container(String id, List<AuthMethod> authMethods, List<TokenId> dataPlaneTokens) {
-
- public Container(String id, List<AuthMethod> authMethods, List<TokenId> dataPlaneTokens) {
- this.id = Objects.requireNonNull(id);
- this.authMethods = Objects.requireNonNull(authMethods).stream()
- .distinct()
- .sorted()
- .toList();
- if (authMethods.isEmpty()) throw new IllegalArgumentException("Container must have at least one auth method");
- this.dataPlaneTokens = dataPlaneTokens.stream().sorted().distinct().toList();
- }
-
- public enum AuthMethod {
- mtls,
- token,
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/LinesComparator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/LinesComparator.java
deleted file mode 100644
index 8b4791c6b1b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/LinesComparator.java
+++ /dev/null
@@ -1,246 +0,0 @@
-/*
- * Line based variant of Apache commons-text StringComparator
- * https://github.com/apache/commons-text/blob/3b1a0a5a47ee9fa2b36f99ca28e2e1d367a10a11/src/main/java/org/apache/commons/text/diff/StringsComparator.java
- */
-
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 com.yahoo.vespa.hosted.controller.application.pkg;
-
-import com.yahoo.collections.Pair;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-/**
- * <p>
- * It is guaranteed that the comparisons will always be done as
- * {@code o1.equals(o2)} where {@code o1} belongs to the first
- * sequence and {@code o2} belongs to the second sequence. This can
- * be important if subclassing is used for some elements in the first
- * sequence and the {@code equals} method is specialized.
- * </p>
- * <p>
- * Comparison can be seen from two points of view: either as giving the smallest
- * modification allowing to transform the first sequence into the second one, or
- * as giving the longest sequence which is a subsequence of both initial
- * sequences. The {@code equals} method is used to compare objects, so any
- * object can be put into sequences. Modifications include deleting, inserting
- * or keeping one object, starting from the beginning of the first sequence.
- * </p>
- * <p>
- * This class implements the comparison algorithm, which is the very efficient
- * algorithm from Eugene W. Myers
- * <a href="http://www.cis.upenn.edu/~bcpierce/courses/dd/papers/diff.ps">
- * An O(ND) Difference Algorithm and Its Variations</a>.
- */
-public class LinesComparator {
-
- private final List<String> left;
- private final List<String> right;
- private final int[] vDown;
- private final int[] vUp;
-
- private LinesComparator(List<String> left, List<String> right) {
- this.left = left;
- this.right = right;
-
- int size = left.size() + right.size() + 2;
- vDown = new int[size];
- vUp = new int[size];
- }
-
- private void buildScript(int start1, int end1, int start2, int end2, List<Pair<LineOperation, String>> result) {
- Snake middle = getMiddleSnake(start1, end1, start2, end2);
-
- if (middle == null
- || middle.start == end1 && middle.diag == end1 - end2
- || middle.end == start1 && middle.diag == start1 - start2) {
-
- int i = start1;
- int j = start2;
- while (i < end1 || j < end2) {
- if (i < end1 && j < end2 && left.get(i).equals(right.get(j))) {
- result.add(new Pair<>(LineOperation.keep, left.get(i)));
- ++i;
- ++j;
- } else {
- if (end1 - start1 > end2 - start2) {
- result.add(new Pair<>(LineOperation.delete, left.get(i)));
- ++i;
- } else {
- result.add(new Pair<>(LineOperation.insert, right.get(j)));
- ++j;
- }
- }
- }
-
- } else {
- buildScript(start1, middle.start, start2, middle.start - middle.diag, result);
- for (int i = middle.start; i < middle.end; ++i) {
- result.add(new Pair<>(LineOperation.keep, left.get(i)));
- }
- buildScript(middle.end, end1, middle.end - middle.diag, end2, result);
- }
- }
-
- private Snake buildSnake(final int start, final int diag, final int end1, final int end2) {
- int end = start;
- while (end - diag < end2 && end < end1 && left.get(end).equals(right.get(end - diag))) {
- ++end;
- }
- return new Snake(start, end, diag);
- }
-
- private Snake getMiddleSnake(final int start1, final int end1, final int start2, final int end2) {
- final int m = end1 - start1;
- final int n = end2 - start2;
- if (m == 0 || n == 0) {
- return null;
- }
-
- final int delta = m - n;
- final int sum = n + m;
- final int offset = (sum % 2 == 0 ? sum : sum + 1) / 2;
- vDown[1 + offset] = start1;
- vUp[1 + offset] = end1 + 1;
-
- for (int d = 0; d <= offset; ++d) {
- // Down
- for (int k = -d; k <= d; k += 2) {
- // First step
-
- final int i = k + offset;
- if (k == -d || k != d && vDown[i - 1] < vDown[i + 1]) {
- vDown[i] = vDown[i + 1];
- } else {
- vDown[i] = vDown[i - 1] + 1;
- }
-
- int x = vDown[i];
- int y = x - start1 + start2 - k;
-
- while (x < end1 && y < end2 && left.get(x).equals(right.get(y))) {
- vDown[i] = ++x;
- ++y;
- }
- // Second step
- if (delta % 2 != 0 && delta - d <= k && k <= delta + d) {
- if (vUp[i - delta] <= vDown[i]) { // NOPMD
- return buildSnake(vUp[i - delta], k + start1 - start2, end1, end2);
- }
- }
- }
-
- // Up
- for (int k = delta - d; k <= delta + d; k += 2) {
- // First step
- final int i = k + offset - delta;
- if (k == delta - d || k != delta + d && vUp[i + 1] <= vUp[i - 1]) {
- vUp[i] = vUp[i + 1] - 1;
- } else {
- vUp[i] = vUp[i - 1];
- }
-
- int x = vUp[i] - 1;
- int y = x - start1 + start2 - k;
- while (x >= start1 && y >= start2 && left.get(x).equals(right.get(y))) {
- vUp[i] = x--;
- y--;
- }
- // Second step
- if (delta % 2 == 0 && -d <= k && k <= d) {
- if (vUp[i] <= vDown[i + delta]) { // NOPMD
- return buildSnake(vUp[i], k + start1 - start2, end1, end2);
- }
- }
- }
- }
-
- // this should not happen
- throw new RuntimeException("Internal Error");
- }
-
- private static class Snake {
- private final int start;
- private final int end;
- private final int diag;
-
- private Snake(int start, int end, int diag) {
- this.start = start;
- this.end = end;
- this.diag = diag;
- }
- }
-
- private enum LineOperation {
- keep(" "), delete("- "), insert("+ ");
- private final String prefix;
- LineOperation(String prefix) {
- this.prefix = prefix;
- }
- }
-
- /** @return line-based diff in unified format. Empty contents are identical. */
- public static Optional<String> diff(List<String> left, List<String> right) {
- List<Pair<LineOperation, String>> changes = new ArrayList<>(Math.max(left.size(), right.size()));
- new LinesComparator(left, right).buildScript(0, left.size(), 0, right.size(), changes);
-
- // After we have a list of keep, delete, insert for each line from left and right input, generate a unified
- // diff by printing all delete and insert operations with contextLines of keep lines before and after.
- // Make sure the change windows are non-overlapping by continuously growing the window
- int contextLines = 3;
- List<int[]> changeWindows = new ArrayList<>();
- int[] last = null;
- for (int i = 0, leftIndex = 0, rightIndex = 0; i < changes.size(); i++) {
- if (changes.get(i).getFirst() == LineOperation.keep) {
- leftIndex++;
- rightIndex++;
- continue;
- }
-
- // We found a new change and it is too far away from the previous change to be combined into the same window
- if (last == null || i - last[1] > contextLines) {
- last = new int[]{Math.max(i - contextLines, 0), Math.min(i + contextLines + 1, changes.size()), Math.max(leftIndex - contextLines, 0), Math.max(rightIndex - contextLines, 0)};
- changeWindows.add(last);
- } else // otherwise, extend the previous change window
- last[1] = Math.min(i + contextLines + 1, changes.size());
-
- if (changes.get(i).getFirst() == LineOperation.delete) leftIndex++;
- else rightIndex++;
- }
- if (changeWindows.isEmpty()) return Optional.empty();
-
- StringBuilder sb = new StringBuilder();
- for (int[] changeWindow: changeWindows) {
- int start = changeWindow[0], end = changeWindow[1], leftIndex = changeWindow[2], rightIndex = changeWindow[3];
- Map<LineOperation, Long> counts = IntStream.range(start, end)
- .mapToObj(i -> changes.get(i).getFirst())
- .collect(Collectors.groupingBy(i -> i, Collectors.counting()));
- sb.append("@@ -").append(leftIndex + 1).append(',').append(end - start - counts.getOrDefault(LineOperation.insert, 0L))
- .append(" +").append(rightIndex + 1).append(',').append(end - start - counts.getOrDefault(LineOperation.delete, 0L)).append(" @@\n");
- for (int i = start; i < end; i++)
- sb.append(changes.get(i).getFirst().prefix).append(changes.get(i).getSecond()).append('\n');
- }
- return Optional.of(sb.toString());
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java
deleted file mode 100644
index dc55472bcc2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackage.java
+++ /dev/null
@@ -1,384 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.DeploymentSpec.Step;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.AthenzService;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.path.Path;
-import com.yahoo.security.KeyAlgorithm;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.security.SignatureAlgorithm;
-import com.yahoo.security.X509CertificateBuilder;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageStream.Replacer;
-import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
-import com.yahoo.vespa.hosted.controller.config.ControllerConfig.Steprunner.Testerapp;
-import com.yahoo.yolean.Exceptions;
-
-import javax.security.auth.x500.X500Principal;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.math.BigInteger;
-import java.security.KeyPair;
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.UUID;
-import java.util.function.Supplier;
-import java.util.function.UnaryOperator;
-import java.util.jar.JarInputStream;
-import java.util.jar.Manifest;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.regex.Pattern;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.production;
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging;
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging_setup;
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.system;
-import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.deploymentFile;
-import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.servicesFile;
-import static java.io.InputStream.nullInputStream;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.requireNonNullElse;
-import static java.util.function.UnaryOperator.identity;
-import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.mapping;
-import static java.util.stream.Collectors.toList;
-
-/**
- * Validation and manipulation of test package.
- *
- * @author jonmv
- */
-public class TestPackage {
-
- private static final Logger log = Logger.getLogger(TestPackage.class.getName());
-
- // Must match exactly the advertised resources of an AWS instance type. Also consider that the container
- // will have ~1.8 GB less memory than equivalent resources in AWS (VESPA-16259).
- static final NodeResources DEFAULT_TESTER_RESOURCES_CLOUD = new NodeResources(2, 8, 50, 0.3, NodeResources.DiskSpeed.any);
- static final NodeResources DEFAULT_TESTER_RESOURCES = new NodeResources(1, 4, 50, 0.3, NodeResources.DiskSpeed.any);
-
- private final ApplicationPackageStream applicationPackageStream;
- private final X509Certificate certificate;
-
- public TestPackage(Supplier<InputStream> inZip, boolean isPublicSystem, CloudName cloud, RunId id, Testerapp testerApp,
- DeploymentSpec fallbackSpec, Instant certificateValidFrom, Duration certificateValidDuration) {
- KeyPair keyPair;
- if (certificateValidFrom != null) {
- keyPair = KeyUtils.generateKeypair(KeyAlgorithm.RSA, 2048);
- X500Principal subject = new X500Principal("CN=" + id.tester().id().toFullString() + "." + id.type() + "." + id.number());
- this.certificate = X509CertificateBuilder.fromKeypair(keyPair,
- subject,
- certificateValidFrom,
- certificateValidFrom.plus(certificateValidDuration),
- SignatureAlgorithm.SHA512_WITH_RSA,
- BigInteger.valueOf(1))
- .build();
- }
- else {
- keyPair = null;
- this.certificate = null;
- }
- this.applicationPackageStream = new ApplicationPackageStream(inZip, () -> name -> name.endsWith(".xml"), () -> new Replacer() {
-
- // Initially skips all declared entries, ensuring they're generated and appended after all input entries.
- final Map<String, UnaryOperator<InputStream>> entries = new LinkedHashMap<>();
- final Map<String, UnaryOperator<InputStream>> replacements = new LinkedHashMap<>();
- boolean hasLegacyTests = false;
- DeploymentSpec containedSpec;
-
- @Override
- public String next() {
- if (entries.isEmpty()) return null;
- String next = entries.keySet().iterator().next();
- replacements.put(next, entries.remove(next));
- return next;
- }
-
- @Override
- public InputStream modify(String name, InputStream in) {
- hasLegacyTests |= name.startsWith("artifacts/") && name.endsWith("-tests.jar");
-
- // Pick out the deployment.xml stored in the package, if any.
- if (entries.containsKey(deploymentFile) && name.equals(deploymentFile))
- containedSpec = DeploymentSpec.fromXml(new InputStreamReader(in));
-
- return entries.containsKey(name) ? null // Skip entry for now, as it will be appended later when we get here again after {@link #next()}.
- : replacements.getOrDefault(name, identity()).apply(in); // Modify entry, if needed.
- }
-
- {
- // Copy contents of submitted application-test.zip, and ensure required directories exist within the zip.
- entries.put("artifacts/.ignore-" + UUID.randomUUID(), __ -> nullInputStream());
- entries.put("tests/.ignore-" + UUID.randomUUID(), __ -> nullInputStream());
-
- entries.put(servicesFile,
- __ -> {
- DeploymentSpec spec = requireNonNullElse(containedSpec, fallbackSpec);
- boolean isEnclave = isPublicSystem && ! spec.cloudAccount(cloud, id.application().instance(), id.type().zone()).isUnspecified();
- return new ByteArrayInputStream(servicesXml( ! isPublicSystem,
- certificateValidFrom != null,
- hasLegacyTests,
- testerResourcesFor(id.type().zone(), spec.requireInstance(id.application().instance()), isEnclave),
- testerApp));
- });
-
- entries.put(deploymentFile,
- __ -> new ByteArrayInputStream(deploymentXml(id.tester(),
- id.application().instance(),
- cloud,
- id.type().zone(),
- requireNonNullElse(containedSpec, fallbackSpec))));
-
- if (certificate != null) {
- entries.put("artifacts/key", __ -> new ByteArrayInputStream(KeyUtils.toPem(keyPair.getPrivate()).getBytes(UTF_8)));
- entries.put("artifacts/cert", __ -> new ByteArrayInputStream(X509CertificateUtils.toPem(certificate).getBytes(UTF_8)));
- }
- }
- });
- }
-
- public ApplicationPackageStream asApplicationPackage() {
- return applicationPackageStream;
- }
-
- public X509Certificate certificate() {
- return Objects.requireNonNull(certificate);
- }
-
- public static TestSummary validateTests(DeploymentSpec spec, byte[] testPackage) {
- return validateTests(expectedSuites(spec.steps()), testPackage);
- }
-
- static TestSummary validateTests(Collection<Suite> expectedSuites, byte[] testPackage) {
- List<String> problems = new ArrayList<>();
- Set<Suite> suites = new LinkedHashSet<>();
- ZipEntries.from(testPackage, __ -> true, 0, false).asList().stream()
- .map(entry -> Path.fromString(entry.name()))
- .collect(groupingBy(path -> path.elements().size() > 1 ? path.elements().get(0) : "",
- mapping(path -> (path.elements().size() > 1 ? path.getChildPath() : path).getRelative(), toList())))
- .forEach((directory, paths) -> {
- switch (directory) {
- case "components": {
- for (String path : paths) {
- if (path.endsWith("-tests.jar")) {
- try {
- byte[] testsJar = ZipEntries.readFile(testPackage, "components/" + path, 1 << 30);
- Manifest manifest = new JarInputStream(new ByteArrayInputStream(testsJar)).getManifest();
- String bundleCategoriesHeader = manifest.getMainAttributes().getValue("X-JDisc-Test-Bundle-Categories");
- if (bundleCategoriesHeader == null) continue;
- for (String suite : bundleCategoriesHeader.split(","))
- if ( ! suite.isBlank()) switch (suite.trim()) {
- case "SystemTest" -> suites.add(system);
- case "StagingSetup" -> suites.add(staging_setup);
- case "StagingTest" -> suites.add(staging);
- case "ProductionTest" -> suites.add(production);
- default -> problems.add("unexpected test suite name '" + suite + "' in bundle manifest");
- }
- }
- catch (Exception e) {
- problems.add("failed reading test bundle manifest: " + Exceptions.toMessageString(e));
- }
- }
- }
- }
- break;
- case "tests": {
- if (paths.stream().anyMatch(Pattern.compile("system-test/.+\\.json").asMatchPredicate())) suites.add(system);
- if (paths.stream().anyMatch(Pattern.compile("staging-setup/.+\\.json").asMatchPredicate())) suites.add(staging_setup);
- if (paths.stream().anyMatch(Pattern.compile("staging-test/.+\\.json").asMatchPredicate())) suites.add(staging);
- if (paths.stream().anyMatch(Pattern.compile("production-test/.+\\.json").asMatchPredicate())) suites.add(production);
- }
- break;
- case "artifacts": {
- if (paths.stream().anyMatch(Pattern.compile(".+-tests.jar").asMatchPredicate()))
- suites.addAll(expectedSuites); // ಠ_ಠ
-
- for (String forbidden : List.of("key", "cert"))
- if (paths.contains(forbidden))
- problems.add("test package contains 'artifacts/" + forbidden +
- "'; this conflicts with credentials used to run tests in Vespa Cloud");
- }
- break;
- }
- });
-
- if (expectedSuites.contains(system) && ! suites.contains(system))
- problems.add("test package has no system tests, but <test /> is declared in deployment.xml");
-
- if (suites.contains(staging) != suites.contains(staging_setup))
- problems.add("test package has " + (suites.contains(staging) ? "staging tests" : "staging setup") +
- ", so it should also include " + (suites.contains(staging) ? "staging setup" : "staging tests"));
- else if (expectedSuites.contains(staging) && ! suites.contains(staging))
- problems.add("test package has no staging setup and tests, but <staging /> is declared in deployment.xml");
-
- if (suites.contains(production) != expectedSuites.contains(production))
- problems.add("test package has " + (suites.contains(production) ? "" : "no ") + "production tests, " +
- "but " + (suites.contains(production) ? "no " : "") + "production tests are declared in deployment.xml");
-
- if ( ! problems.isEmpty())
- problems.add("see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa");
-
- return new TestSummary(problems, suites);
- }
-
- static NodeResources testerResourcesFor(ZoneId zone, DeploymentInstanceSpec spec, boolean isEnclave) {
- NodeResources nodeResources = spec.steps().stream()
- .filter(step -> step.concerns(zone.environment()))
- .findFirst()
- .flatMap(step -> step.zones().get(0).testerFlavor())
- .map(NodeResources::fromLegacyName)
- .orElse(zone.region().value().matches("^(aws|gcp)-.*") ? DEFAULT_TESTER_RESOURCES_CLOUD
- : DEFAULT_TESTER_RESOURCES);
- if (isEnclave) nodeResources = nodeResources.with(NodeResources.Architecture.x86_64);
- return nodeResources.with(NodeResources.DiskSpeed.any);
- }
-
- /** Returns the generated services.xml content for the tester application. */
- static byte[] servicesXml(boolean systemUsesAthenz, boolean useTesterCertificate, boolean hasLegacyTests,
- NodeResources resources, ControllerConfig.Steprunner.Testerapp config) {
- int jdiscMemoryGb = 2; // 2Gb memory for tester application which uses Maven.
- int jdiscMemoryPct = (int) Math.ceil(100 * jdiscMemoryGb / resources.memoryGb());
-
- // Of the remaining memory, split 50/50 between Surefire running the tests and the rest
- int testMemoryMb = (int) (1024 * (resources.memoryGb() - jdiscMemoryGb) / 2);
-
- String resourceString = Text.format("<resources vcpu=\"%.2f\" memory=\"%.2fGb\" disk=\"%.2fGb\" disk-speed=\"%s\" storage-type=\"%s\" architecture=\"%s\"/>",
- resources.vcpu(), resources.memoryGb(), resources.diskGb(), resources.diskSpeed().name(), resources.storageType().name(), resources.architecture().name());
-
- String runtimeProviderClass = config.runtimeProviderClass();
- String tenantCdBundle = config.tenantCdBundle();
-
- String servicesXml =
- "<?xml version='1.0' encoding='UTF-8'?>\n" +
- "<services xmlns:deploy='vespa' version='1.0'>\n" +
- " <container version='1.0' id='tester'>\n" +
- "\n" +
- " <component id=\"com.yahoo.vespa.hosted.testrunner.TestRunner\" bundle=\"vespa-testrunner-components\">\n" +
- " <config name=\"com.yahoo.vespa.hosted.testrunner.test-runner\">\n" +
- " <artifactsPath>artifacts</artifactsPath>\n" +
- " <surefireMemoryMb>" + testMemoryMb + "</surefireMemoryMb>\n" +
- " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
- " <useTesterCertificate>" + useTesterCertificate + "</useTesterCertificate>\n" +
- " </config>\n" +
- " </component>\n" +
- "\n" +
- " <handler id=\"com.yahoo.vespa.testrunner.TestRunnerHandler\" bundle=\"vespa-osgi-testrunner\">\n" +
- " <binding>http://*/tester/v1/*</binding>\n" +
- " </handler>\n" +
- "\n" +
- " <component id=\"" + runtimeProviderClass + "\" bundle=\"" + tenantCdBundle + "\" />\n" +
- "\n" +
- " <component id=\"com.yahoo.vespa.testrunner.JunitRunner\" bundle=\"vespa-osgi-testrunner\">\n" +
- " <config name=\"com.yahoo.vespa.testrunner.junit-test-runner\">\n" +
- " <artifactsPath>artifacts</artifactsPath>\n" +
- " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
- " </config>\n" +
- " </component>\n" +
- "\n" +
- " <component id=\"com.yahoo.vespa.testrunner.VespaCliTestRunner\" bundle=\"vespa-osgi-testrunner\">\n" +
- " <config name=\"com.yahoo.vespa.testrunner.vespa-cli-test-runner\">\n" +
- " <artifactsPath>artifacts</artifactsPath>\n" +
- " <testsPath>tests</testsPath>\n" +
- " <useAthenzCredentials>" + systemUsesAthenz + "</useAthenzCredentials>\n" +
- " </config>\n" +
- " </component>\n" +
- "\n" +
- " <nodes count=\"1\">\n" +
- (hasLegacyTests ? " <jvm allocated-memory=\"" + jdiscMemoryPct + "%\"/>\n" : "" ) +
- " " + resourceString + "\n" +
- " </nodes>\n" +
- " </container>\n" +
- "</services>\n";
-
- return servicesXml.getBytes(UTF_8);
- }
-
- /** Returns a dummy deployment xml which sets up the service identity for the tester, if present. */
- static byte[] deploymentXml(TesterId id, InstanceName instance, CloudName cloud, ZoneId zone, DeploymentSpec original) {
- Optional<AthenzDomain> athenzDomain = original.athenzDomain();
- Optional<AthenzService> athenzService = original.requireInstance(instance)
- .athenzService(zone.environment(), zone.region());
- Optional<CloudAccount> cloudAccount = Optional.of(original.cloudAccount(cloud, instance, zone))
- .filter(account -> ! account.isUnspecified());
- Optional<Duration> hostTTL = (zone.environment().isProduction()
- ? original.requireInstance(instance)
- .steps().stream().filter(step -> step.isTest() && step.concerns(zone.environment(), Optional.of(zone.region())))
- .findFirst().flatMap(Step::hostTTL)
- : original.requireInstance(instance).hostTTL(zone.environment(), Optional.of(zone.region())))
- .filter(__ -> cloudAccount.isPresent());
- String deploymentSpec =
- "<?xml version='1.0' encoding='UTF-8'?>\n" +
- "<deployment version='1.0'" +
- athenzDomain.map(domain -> " athenz-domain='" + domain.value() + "'").orElse("") +
- athenzService.map(service -> " athenz-service='" + service.value() + "'").orElse("") +
- cloudAccount.map(account -> " cloud-account='" + account.value() + "'").orElse("") +
- hostTTL.map(ttl -> " empty-host-ttl='" + ttl.getSeconds() / 60 + "m'").orElse("") +
- ">" +
- " <instance id='" + id.id().instance().value() + "' />" +
- "</deployment>";
- return deploymentSpec.getBytes(UTF_8);
- }
-
- static Set<Suite> expectedSuites(List<Step> steps) {
- Set<Suite> suites = new HashSet<>();
- if (steps.isEmpty()) return suites;
- for (Step step : steps) {
- if (step.isTest()) {
- if (step.concerns(Environment.prod)) suites.add(production);
- if (step.concerns(Environment.test)) suites.add(system);
- if (step.concerns(Environment.staging)) { suites.add(staging); suites.add(staging_setup); }
- }
- else
- suites.addAll(expectedSuites(step.steps()));
- }
- return suites;
- }
-
-
- public static class TestSummary {
-
- private final List<String> problems;
- private final List<Suite> suites;
-
- public TestSummary(List<String> problems, Set<Suite> suites) {
- this.problems = List.copyOf(problems);
- this.suites = List.copyOf(suites);
- }
-
- public List<String> problems() {
- return problems;
- }
-
- public List<Suite> suites() {
- return suites;
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java
deleted file mode 100644
index 90e7acf9e77..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/pkg/ZipEntries.java
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import com.yahoo.vespa.archive.ArchiveStreamReader;
-import com.yahoo.vespa.archive.ArchiveStreamReader.ArchiveFile;
-import com.yahoo.vespa.archive.ArchiveStreamReader.Options;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.function.Predicate;
-
-/**
- * A list of entries read from a ZIP archive, and their contents.
- *
- * @author bratseth
- */
-public class ZipEntries {
-
- private final List<ZipEntryWithContent> entries;
-
- private ZipEntries(List<ZipEntryWithContent> entries) {
- this.entries = List.copyOf(Objects.requireNonNull(entries));
- }
-
- /** Read ZIP entries from inputStream */
- public static ZipEntries from(byte[] zip, Predicate<String> entryNameMatcher, int maxEntrySizeInBytes, boolean throwIfEntryExceedsMaxSize) {
-
- Options options = Options.standard()
- .pathPredicate(entryNameMatcher)
- .maxSize(2L << 30) // 2 GB
- .maxEntrySize(maxEntrySizeInBytes)
- .maxEntries(1024)
- .truncateEntry(!throwIfEntryExceedsMaxSize);
- List<ZipEntryWithContent> entries = new ArrayList<>();
- try (ArchiveStreamReader reader = ArchiveStreamReader.ofZip(new ByteArrayInputStream(zip), options)) {
- ArchiveFile file;
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- while ((file = reader.readNextTo(baos)) != null) {
- entries.add(new ZipEntryWithContent(file.path().toString(),
- Optional.of(baos.toByteArray()).filter(b -> b.length > 0),
- file.size()));
- baos.reset();
- }
- }
- return new ZipEntries(entries);
- }
-
- public static byte[] readFile(byte[] zip, String name, int maxEntrySizeInBytes) {
- return from(zip, name::equals, maxEntrySizeInBytes, true).asList().get(0).contentOrThrow();
- }
-
- public List<ZipEntryWithContent> asList() { return entries; }
-
- public static class ZipEntryWithContent {
-
- private final String name;
- private final Optional<byte[]> content;
- private final long size;
-
- public ZipEntryWithContent(String name, Optional<byte[]> content, long size) {
- this.name = name;
- this.content = content;
- this.size = size;
- }
-
- public String name() { return name; }
- public byte[] contentOrThrow() { return content.orElseThrow(() -> new NoSuchElementException("'" + name + "' has no content")); }
- public Optional<byte[]> content() { return content; }
- public long size() { return size; }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDb.java
deleted file mode 100644
index d2be561a520..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDb.java
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.archive;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.TenantManagedArchiveBucket;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.VespaManagedArchiveBucket;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-
-import java.net.URI;
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.stream.Collectors;
-
-/**
- * This class decides which tenant goes in what bucket, and creates new buckets when required.
- *
- * @author andreer
- */
-public class CuratorArchiveBucketDb {
-
- private static final Duration ENCLAVE_BUCKET_CACHE_LIFETIME = Duration.ofMinutes(60);
-
- /**
- * Archive URIs are often requested because they are returned in /application/v4 API. Since they
- * never change, it's safe to cache them and only update on misses
- */
- private final Map<ZoneId, Map<TenantName, String>> archiveUriCache = new ConcurrentHashMap<>();
- private final Map<ZoneId, Map<CloudAccount, TenantManagedArchiveBucket>> tenantArchiveCache = new ConcurrentHashMap<>();
-
- private final ArchiveService archiveService;
- private final CuratorDb curatorDb;
- private final Clock clock;
-
- public CuratorArchiveBucketDb(Controller controller) {
- this.archiveService = controller.serviceRegistry().archiveService();
- this.curatorDb = controller.curator();
- this.clock = controller.clock();
- }
-
- public Optional<URI> archiveUriFor(ZoneId zoneId, TenantName tenant, boolean createIfMissing) {
- return getBucketNameFromCache(zoneId, tenant)
- .or(() -> createIfMissing ? Optional.of(assignToBucket(zoneId, tenant)) : Optional.empty())
- .map(bucketName -> archiveService.bucketURI(zoneId, bucketName));
- }
-
- public Optional<URI> archiveUriFor(ZoneId zoneId, CloudAccount account, boolean searchIfMissing) {
- Instant updatedAfter = searchIfMissing ? clock.instant().minus(ENCLAVE_BUCKET_CACHE_LIFETIME) : Instant.MIN;
- return getBucketNameFromCache(zoneId, account, updatedAfter)
- .or(() -> {
- if (!searchIfMissing) return Optional.empty();
- try (var lock = curatorDb.lockArchiveBuckets(zoneId)) {
- ArchiveBuckets archiveBuckets = buckets(zoneId);
- updateArchiveUriCache(zoneId, archiveBuckets);
-
- return getBucketNameFromCache(zoneId, account, updatedAfter)
- .or(() -> archiveService.findEnclaveArchiveBucket(zoneId, account)
- .map(bucketName -> {
- var bucket = new TenantManagedArchiveBucket(bucketName, account, clock.instant());
- ArchiveBuckets updated = archiveBuckets.with(bucket);
- curatorDb.writeArchiveBuckets(zoneId, updated);
- updateArchiveUriCache(zoneId, updated);
- return bucket;
- }));
- }
- })
- .map(TenantManagedArchiveBucket::bucketName)
- .map(bucketName -> archiveService.bucketURI(zoneId, bucketName));
- }
-
- private String assignToBucket(ZoneId zoneId, TenantName tenant) {
- try (var lock = curatorDb.lockArchiveBuckets(zoneId)) {
- ArchiveBuckets archiveBuckets = buckets(zoneId);
- updateArchiveUriCache(zoneId, archiveBuckets);
-
- return getBucketNameFromCache(zoneId, tenant) // Some other thread might have assigned it before we grabbed the lock
- .orElseGet(() -> {
- // If not, find an existing bucket with space
- VespaManagedArchiveBucket bucketToAssignTo = archiveBuckets.vespaManaged().stream()
- .filter(bucket -> archiveService.canAddTenantToBucket(zoneId, bucket))
- .findAny()
- // Or create a new one
- .orElseGet(() -> archiveService.createArchiveBucketFor(zoneId));
-
- ArchiveBuckets updated = archiveBuckets.with(bucketToAssignTo.withTenant(tenant));
- curatorDb.writeArchiveBuckets(zoneId, updated);
- updateArchiveUriCache(zoneId, updated);
-
- return bucketToAssignTo.bucketName();
- });
- }
- }
-
- public ArchiveBuckets buckets(ZoneId zoneId) {
- return curatorDb.readArchiveBuckets(zoneId);
- }
-
- private Optional<String> getBucketNameFromCache(ZoneId zoneId, TenantName tenantName) {
- return Optional.ofNullable(archiveUriCache.get(zoneId)).map(map -> map.get(tenantName));
- }
-
- private Optional<TenantManagedArchiveBucket> getBucketNameFromCache(ZoneId zoneId, CloudAccount cloudAccount, Instant updatedAfter) {
- return Optional.ofNullable(tenantArchiveCache.get(zoneId))
- .map(map -> map.get(cloudAccount))
- .filter(bucket -> bucket.updatedAt().isAfter(updatedAfter));
- }
-
- private void updateArchiveUriCache(ZoneId zoneId, ArchiveBuckets archiveBuckets) {
- Map<TenantName, String> bucketNameByTenant = archiveBuckets.vespaManaged().stream()
- .flatMap(bucket -> bucket.tenants().stream().map(tenant -> Map.entry(tenant, bucket.bucketName())))
- .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
- archiveUriCache.put(zoneId, bucketNameByTenant);
-
- Map<CloudAccount, TenantManagedArchiveBucket> bucketByAccount = archiveBuckets.tenantManaged().stream()
- .collect(Collectors.toUnmodifiableMap(TenantManagedArchiveBucket::cloudAccount, bucket -> bucket));
- tenantArchiveCache.put(zoneId, bucketByAccount);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/HostedAthenzIdentities.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/HostedAthenzIdentities.java
deleted file mode 100644
index f68c13ec0d4..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/HostedAthenzIdentities.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.athenz;
-
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzService;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-
-/**
- * @author bjorncs
- */
-public class HostedAthenzIdentities {
-
- public static final AthenzDomain SCREWDRIVER_DOMAIN = new AthenzDomain("cd.screwdriver.project");
-
- private HostedAthenzIdentities() {}
-
- public static AthenzUser from(UserId userId) {
- return AthenzUser.fromUserId(userId.id());
- }
-
- public static AthenzService from(ScrewdriverId screwdriverId) {
- return new AthenzService(SCREWDRIVER_DOMAIN, "sd" + screwdriverId.id());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/config/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/config/package-info.java
deleted file mode 100644
index aceee5f70f4..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/config/package-info.java
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * Required for using {@link com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig} outside controller-server module.
- *
- * @author bjorncs
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.athenz.config;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzClientFactoryImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzClientFactoryImpl.java
deleted file mode 100644
index e3f53b5606f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzClientFactoryImpl.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.athenz.impl;
-
-import ai.vespa.metrics.ControllerMetrics;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.client.ErrorHandler;
-import com.yahoo.vespa.athenz.client.zms.DefaultZmsClient;
-import com.yahoo.vespa.athenz.client.zms.ZmsClient;
-import com.yahoo.vespa.athenz.client.zts.DefaultZtsClient;
-import com.yahoo.vespa.athenz.client.zts.ZtsClient;
-import com.yahoo.vespa.athenz.identity.ServiceIdentityProvider;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.athenz.config.AthenzConfig;
-
-import java.net.URI;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * @author bjorncs
- */
-public class AthenzClientFactoryImpl implements AthenzClientFactory {
-
- private static final String METRIC_NAME = ControllerMetrics.ATHENZ_REQUEST_ERROR.baseName();
- private static final String ATHENZ_SERVICE_DIMENSION = "athenz-service";
- private static final String EXCEPTION_DIMENSION = "exception";
-
- private final AthenzConfig config;
- private final ServiceIdentityProvider identityProvider;
- private final Metric metrics;
- private final Map<String, Metric.Context> metricContexts;
-
- @Inject
- public AthenzClientFactoryImpl(ServiceIdentityProvider identityProvider, AthenzConfig config, Metric metrics) {
- this.identityProvider = identityProvider;
- this.config = config;
- this.metrics = metrics;
- this.metricContexts = new HashMap<>();
- }
-
- @Override
- public AthenzIdentity getControllerIdentity() {
- return identityProvider.identity();
- }
-
- /**
- * @return A ZMS client instance with the service identity as principal.
- */
- @Override
- public ZmsClient createZmsClient() {
- return new DefaultZmsClient(URI.create(config.zmsUrl()), identityProvider, this::reportMetricErrorHandler);
- }
-
- /**
- * @return A ZTS client instance with the service identity as principal.
- */
- @Override
- public ZtsClient createZtsClient() {
- return new DefaultZtsClient.Builder(URI.create(config.ztsUrl())).withIdentityProvider(identityProvider).build();
- }
-
- @Override
- public boolean cacheLookups() {
- return true;
- }
-
- private void reportMetricErrorHandler(ErrorHandler.RequestProperties request, Exception error) {
- Metric.Context context = metricContexts.computeIfAbsent(request.hostname(), host -> metrics.createContext(
- Map.of(ATHENZ_SERVICE_DIMENSION, host,
- EXCEPTION_DIMENSION, error.getClass().getSimpleName())));
- metrics.add(METRIC_NAME, 1, context);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
deleted file mode 100644
index ec5fb9af902..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/athenz/impl/AthenzFacade.java
+++ /dev/null
@@ -1,372 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.athenz.impl;
-
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.restapi.RestApiException;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.AthenzResourceName;
-import com.yahoo.vespa.athenz.api.AthenzRole;
-import com.yahoo.vespa.athenz.api.AthenzService;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-import com.yahoo.vespa.athenz.client.zms.RoleAction;
-import com.yahoo.vespa.athenz.client.zms.ZmsClient;
-import com.yahoo.vespa.athenz.client.zms.ZmsClientException;
-import com.yahoo.vespa.athenz.client.zts.ZtsClient;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.security.AccessControl;
-import com.yahoo.vespa.hosted.controller.security.AthenzCredentials;
-import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec;
-import com.yahoo.vespa.hosted.controller.security.Credentials;
-import com.yahoo.vespa.hosted.controller.security.TenantSpec;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * @author bjorncs
- * @author jonmv
- */
-public class AthenzFacade implements AccessControl {
-
- private static final Logger log = Logger.getLogger(AthenzFacade.class.getName());
- private final ZmsClient zmsClient;
- private final ZtsClient ztsClient;
- private final AthenzIdentity service;
- private final Function<AthenzIdentity, List<AthenzDomain>> userDomains;
- private final Predicate<AccessTuple> accessRights;
-
- @Inject
- public AthenzFacade(AthenzClientFactory factory) {
- this.zmsClient = factory.createZmsClient();
- this.ztsClient = factory.createZtsClient();
- this.service = factory.getControllerIdentity();
- this.userDomains = factory.cacheLookups()
- ? CacheBuilder.newBuilder()
- .expireAfterWrite(10, TimeUnit.SECONDS)
- .build(CacheLoader.from(this::getUserDomains))::getUnchecked
- : this::getUserDomains;
- this.accessRights = factory.cacheLookups()
- ? CacheBuilder.newBuilder()
- .expireAfterWrite(10, TimeUnit.SECONDS)
- .build(CacheLoader.from(this::lookupAccess))::getUnchecked
- : this::lookupAccess;
- }
-
- private List<AthenzDomain> getUserDomains(AthenzIdentity userIdentity) {
- return ztsClient.getTenantDomains(service, userIdentity, "admin");
- }
-
- @Override
- public Tenant createTenant(TenantSpec tenantSpec, Instant createdAt, Credentials credentials, List<Tenant> existing) {
- AthenzTenantSpec spec = (AthenzTenantSpec) tenantSpec;
- AthenzCredentials athenzCredentials = (AthenzCredentials) credentials;
- AthenzDomain domain = spec.domain();
-
- verifyIsDomainAdmin(athenzCredentials.user().getIdentity(), domain);
-
- Optional<Tenant> existingWithSameDomain = existing.stream()
- .filter(tenant -> tenant.type() == Tenant.Type.athenz
- && domain.equals(((AthenzTenant) tenant).domain()))
- .findAny();
-
- AthenzTenant tenant = AthenzTenant.create(spec.tenant(),
- domain,
- spec.property(),
- spec.propertyId(),
- createdAt);
-
- if (existingWithSameDomain.isPresent()) { // Throw if domain is already taken.
- throw new IllegalArgumentException("Could not create tenant '" + spec.tenant().value() +
- "': The Athens domain '" +
- domain.getName() + "' is already connected to tenant '" +
- existingWithSameDomain.get().name().value() + "'");
- }
- else { // Create tenant resources in Athenz if domain is not already taken.
- log("createTenancy(tenantDomain=%s, service=%s)", domain, service);
- zmsClient.createTenancy(domain, service, athenzCredentials.oAuthCredentials());
- }
-
- return tenant;
- }
-
- @Override
- public Tenant updateTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing, List<Application> applications) {
- AthenzTenantSpec spec = (AthenzTenantSpec) tenantSpec;
- AthenzCredentials athenzCredentials = (AthenzCredentials) credentials;
- AthenzDomain newDomain = spec.domain();
- AthenzDomain oldDomain = athenzCredentials.domain();
-
- verifyIsDomainAdmin(athenzCredentials.user().getIdentity(), newDomain);
-
- Optional<Tenant> existingWithSameDomain = existing.stream()
- .filter(tenant -> tenant.type() == Tenant.Type.athenz
- && newDomain.equals(((AthenzTenant) tenant).domain()))
- .findAny();
- Instant createdAt = existing.stream()
- .filter(tenant -> tenant.name().equals(spec.tenant()))
- .findAny().orElseThrow() // Should not happen, we assert that the tenant exists before the method is called
- .createdAt();
-
- Tenant tenant = AthenzTenant.create(spec.tenant(),
- newDomain,
- spec.property(),
- spec.propertyId(),
- createdAt);
-
- if (existingWithSameDomain.isPresent()) { // Throw if domain taken by someone else, or do nothing if taken by this tenant.
- if ( ! existingWithSameDomain.get().equals(tenant)) // Equality by name.
- throw new IllegalArgumentException("Could not create tenant '" + spec.tenant().value() +
- "': The Athens domain '" +
- newDomain.getName() + "' is already connected to tenant '" +
- existingWithSameDomain.get().name().value() + "'");
-
- return tenant; // Short-circuit here if domain is still the same.
- }
- else { // Delete and recreate tenant, and optionally application, resources in Athenz otherwise.
- log("createTenancy(tenantDomain=%s, service=%s)", newDomain, service);
- zmsClient.createTenancy(newDomain, service, athenzCredentials.oAuthCredentials());
- for (Application application : applications)
- createApplication(newDomain, application.id().application(), athenzCredentials.oAuthCredentials());
-
- log("deleteTenancy(tenantDomain=%s, service=%s)", oldDomain, service);
- for (Application application : applications)
- deleteApplication(oldDomain, application.id().application(), athenzCredentials.oAuthCredentials());
- zmsClient.deleteTenancy(oldDomain, service, athenzCredentials.oAuthCredentials());
- }
-
- return tenant;
- }
-
- @Override
- public void deleteTenant(TenantName tenant, Credentials credentials) {
- AthenzCredentials athenzCredentials = (AthenzCredentials) credentials;
- AthenzDomain tenantDomain = athenzCredentials.domain();
- log("deleteTenancy(tenantDomain=%s, service=%s)", tenantDomain, service);
- try {
- zmsClient.deleteTenancy(tenantDomain, service, athenzCredentials.oAuthCredentials());
- } catch (ZmsClientException e) {
- if (e.getErrorCode() == 404) {
- log.log(Level.WARNING,
- "Failed to cleanup tenant " + tenant.value() + " with domain '" + tenantDomain.getName()
- + "' in Athenz due to non-existing tenant domain",
- e);
- } else {
- throw e;
- }
- }
- }
-
- @Override
- public void createApplication(TenantAndApplicationId id, Credentials credentials) {
- AthenzCredentials athenzCredentials = (AthenzCredentials) credentials;
- createApplication(athenzCredentials.domain(), id.application(), athenzCredentials.oAuthCredentials());
- }
-
- private void createApplication(AthenzDomain domain, ApplicationName application, OAuthCredentials oAuthCredentials) {
- Set<RoleAction> tenantRoleActions = createTenantRoleActions();
- log("createProviderResourceGroup(" +
- "tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s, roleActions=%s)",
- domain, service.getDomain().getName(), service.getName(), application, tenantRoleActions);
- try {
- zmsClient.createProviderResourceGroup(domain, service, application.value(), tenantRoleActions, oAuthCredentials);
- }
- catch (ZmsClientException e) {
- if (e.getErrorCode() == com.yahoo.jdisc.Response.Status.FORBIDDEN)
- throw new RestApiException.Forbidden("Not authorized to create application", e);
- else
- throw e;
- }
- }
-
- @Override
- public void deleteApplication(TenantAndApplicationId id, Credentials credentials) {
- AthenzCredentials athenzCredentials = (AthenzCredentials) credentials;
- log("deleteProviderResourceGroup(tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s)",
- athenzCredentials.domain(), service.getDomain().getName(), service.getName(), id.application());
- try {
- zmsClient.deleteProviderResourceGroup(athenzCredentials.domain(), service, id.application().value(),
- athenzCredentials.oAuthCredentials());
- } catch (ZmsClientException e) {
- if (e.getErrorCode() == 404) {
- log.log(Level.WARNING,
- "Failed to cleanup application '" + id.serialized()
- + "' in Athenz due to non-existing tenant domain or resource group",
- e);
- } else {
- throw e;
- }
- }
- }
-
- /**
- * Returns the list of tenants to which a user has access.
- * @param tenants the list of all known tenants
- * @param credentials the credentials of user whose tenants to list
- * @return the list of tenants the given user has access to
- */
- // TODO jonmv: Remove
- public List<Tenant> accessibleTenants(List<Tenant> tenants, Credentials credentials) {
- AthenzIdentity identity = ((AthenzPrincipal) credentials.user()).getIdentity();
- return tenants.stream()
- .filter(tenant -> tenant.type() == Tenant.Type.athenz
- && userDomains.apply(identity).contains(((AthenzTenant) tenant).domain()))
- .toList();
- }
-
- public void addTenantAdmin(AthenzDomain tenantDomain, AthenzUser user) {
- zmsClient.addRoleMember(new AthenzRole(tenantDomain, "tenancy." + service.getFullName() + ".admin"), user, Optional.empty());
- }
-
- private void deleteApplication(AthenzDomain domain, ApplicationName application, OAuthCredentials oAuthCredentials) {
- log("deleteProviderResourceGroup(tenantDomain=%s, providerDomain=%s, service=%s, resourceGroup=%s)",
- domain, service.getDomain().getName(), service.getName(), application);
- zmsClient.deleteProviderResourceGroup(domain, service, application.value(), oAuthCredentials);
- }
-
- public boolean hasApplicationAccess(
- AthenzIdentity identity, ApplicationAction action, AthenzDomain tenantDomain, ApplicationName applicationName, Optional<ZoneId> zone) {
- return hasAccess(
- action.name(), applicationResourceString(tenantDomain, applicationName, zone), identity);
- }
-
- public boolean hasTenantAdminAccess(AthenzIdentity identity, AthenzDomain tenantDomain) {
- return hasAccess(TenantAction._modify_.name(), tenantResourceString(tenantDomain), identity);
- }
-
- public boolean hasHostedOperatorAccess(AthenzIdentity identity) {
- return hasAccess("modify", service.getDomain().getName() + ":hosted-vespa", identity);
- }
-
- public boolean hasHostedSupporterAccess(AthenzIdentity identity) {
- return hasAccess("read", service.getDomain().getName() + ":hosted-vespa", identity);
- }
-
- public boolean canLaunch(AthenzIdentity principal, AthenzService service) {
- return hasAccess("launch", service.getDomain().getName() + ":service."+service.getName(), principal);
- }
-
- public boolean hasSystemFlagsAccess(AthenzIdentity identity, boolean dryRun) {
- return hasAccess(dryRun ? "dryrun" : "deploy", new AthenzResourceName(service.getDomain(), "system-flags").toResourceNameString(), identity);
- }
-
- public boolean hasPaymentCallbackAccess(AthenzIdentity identity) {
- return hasAccess("callback", new AthenzResourceName(service.getDomain().getName(), "payment-notification-resource").toResourceNameString(), identity);
- }
-
- public boolean hasAccountingAccess(AthenzIdentity identity) {
- return hasAccess("modify", new AthenzResourceName(service.getDomain().getName(), "hosted-accounting-resource").toResourceNameString(), identity);
- }
-
- /**
- * Used when creating tenancies. As there are no tenancy policies at this point,
- * we cannot use {@link #hasTenantAdminAccess(AthenzIdentity, AthenzDomain)}
- */
- private void verifyIsDomainAdmin(AthenzIdentity identity, AthenzDomain domain) {
- log("getMembership(domain=%s, role=%s, principal=%s)", domain, "admin", identity);
- if ( ! zmsClient.getMembership(new AthenzRole(domain, "admin"), identity))
- throw new RestApiException.Forbidden(
- Text.format("The user '%s' is not admin in Athenz domain '%s'", identity.getFullName(), domain.getName()));
- }
-
- public List<AthenzDomain> getDomainList(String prefix) {
- log.log(Level.FINE, "getDomainList(prefix=%s)", prefix);
- return zmsClient.getDomainList(prefix);
- }
-
- private static Set<RoleAction> createTenantRoleActions() {
- return Arrays.stream(ApplicationAction.values())
- .map(action -> new RoleAction(action.roleName, action.name()))
- .collect(Collectors.toSet());
- }
-
- private boolean hasAccess(String action, String resource, AthenzIdentity identity) {
- return accessRights.test(new AccessTuple(resource, action, identity));
- }
-
- private boolean lookupAccess(AccessTuple t) {
- boolean result = ztsClient.hasAccess(AthenzResourceName.fromString(t.resource), t.action, t.identity);
- log("getAccess(action=%s, resource=%s, principal=%s) = %b", t.action, t.resource, t.identity, result);
- return result;
- }
-
- private static void log(String format, Object... args) {
- log.log(Level.FINE, String.format(format, args));
- }
-
- private String resourceStringPrefix(AthenzDomain tenantDomain) {
- return Text.format("%s:service.%s.tenant.%s",
- service.getDomain().getName(), service.getName(), tenantDomain.getName());
- }
-
- private String tenantResourceString(AthenzDomain tenantDomain) {
- return resourceStringPrefix(tenantDomain) + ".wildcard";
- }
-
- private String applicationResourceString(AthenzDomain tenantDomain, ApplicationName applicationName, Optional<ZoneId> zone) {
- // If environment is not provided, add .wildcard to match .* in the policy resource (* is not allowed in the request)
- String environment = zone.map(ZoneId::environment).map(Environment::value).orElse("wildcard");
- return resourceStringPrefix(tenantDomain) + "." + "res_group" + "." + applicationName.value() + "." + environment;
- }
-
- private enum TenantAction {
- // This is meant to match only the '*' action of the 'admin' role.
- // If needed, we can replace it with 'create', 'delete' etc. later.
- _modify_
- }
-
-
- private static class AccessTuple {
-
- private final String resource;
- private final String action;
- private final AthenzIdentity identity;
-
- private AccessTuple(String resource, String action, AthenzIdentity identity) {
- this.resource = resource;
- this.action = action;
- this.identity = identity;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- AccessTuple that = (AccessTuple) o;
- return resource.equals(that.resource) &&
- action.equals(that.action) &&
- identity.equals(that.identity);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(resource, action, identity);
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLog.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLog.java
deleted file mode 100644
index dfc660442b9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLog.java
+++ /dev/null
@@ -1,147 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.auditlog;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * This represents the audit log of a hosted Vespa system. The audit log contains manual actions performed through
- * operator APIs served by the controller.
- *
- * Entries of the audit log are sorted by their timestamp, in descending order.
- *
- * @author mpolden
- */
-public record AuditLog(List<Entry> entries) {
-
- public static final AuditLog empty = new AuditLog(List.of());
-
- /** DO NOT USE. Public for serialization purposes */
- public AuditLog(List<Entry> entries) {
- this.entries = Objects.requireNonNull(entries).stream().sorted().toList();
- }
-
- /** Returns a new audit log without entries older than given instant */
- public AuditLog pruneBefore(Instant instant) {
- List<Entry> entries = new ArrayList<>(this.entries);
- entries.removeIf(entry -> entry.at().isBefore(instant));
- return new AuditLog(entries);
- }
-
- /** Returns copy of this with given entry added */
- public AuditLog with(Entry entry) {
- List<Entry> entries = new ArrayList<>(this.entries);
- entries.add(entry);
- return new AuditLog(entries);
- }
-
- /** Returns the first n entries in this. Since entries are sorted descendingly, this will be the n newest entries */
- public AuditLog first(int n) {
- if (entries.size() < n) return this;
- return new AuditLog(entries.subList(0, n));
- }
-
- /** An entry in the audit log. This describes an HTTP request */
- public record Entry(Instant at, String principal, Method method, String resource, Optional<String> data,
- Client client) implements Comparable<Entry> {
-
- final static int maxDataLength = 1024;
- private final static Comparator<Entry> comparator = Comparator.comparing(Entry::at).reversed();
-
- public Entry(Instant at, Client client, String principal, Method method, String resource, byte[] data) {
- this(Objects.requireNonNull(at, "at must be non-null"),
- Objects.requireNonNull(principal, "principal must be non-null"),
- Objects.requireNonNull(method, "method must be non-null"),
- Objects.requireNonNull(resource, "resource must be non-null"),
- sanitize(data),
- Objects.requireNonNull(client, "client must be non-null"));
- }
-
- /** Time of the request */
- public Instant at() {
- return at;
- }
-
- /**
- * The client that performed this request. This may be based on user-controlled input, e.g. User-Agent header
- * and is thus not guaranteed to be accurate.
- */
- public Client client() {
- return client;
- }
-
- /** The principal performing the request */
- public String principal() {
- return principal;
- }
-
- /** Request method */
- public Method method() {
- return method;
- }
-
- /** API resource (URL path) */
- public String resource() {
- return resource;
- }
-
- /** Request data. This may be truncated if request data logged in this entry was too large */
- public Optional<String> data() {
- return data;
- }
-
- @Override
- public int compareTo(Entry that) {
- return comparator.compare(this, that);
- }
-
- /** HTTP methods that should be logged */
- public enum Method {
- POST,
- PATCH,
- PUT,
- DELETE
- }
-
- /** Known clients of the audit log */
- public enum Client {
- /** The Vespa Cloud Console */
- console,
- /** Vespa CLI */
- cli,
- /** Operator tools */
- hv,
- /** Other clients, e.g. curl */
- other,
- }
-
- private static Optional<String> sanitize(byte[] data) {
- StringBuilder sb = new StringBuilder();
- for (byte b : data) {
- char c = (char) b;
- if (!printableAscii(c) && !tabOrLineBreak(c)) {
- return Optional.empty();
- }
- sb.append(c);
- if (sb.length() == maxDataLength) {
- break;
- }
- }
- return Optional.of(sb.toString()).filter(s -> !s.isEmpty());
- }
-
- private static boolean printableAscii(char c) {
- return c >= 32 && c <= 126;
- }
-
- private static boolean tabOrLineBreak(char c) {
- return c == 9 || c == 10 || c == 13;
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLogger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLogger.java
deleted file mode 100644
index ad541599475..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLogger.java
+++ /dev/null
@@ -1,119 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.auditlog;
-
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.jdisc.http.HttpHeaders;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLog.Entry;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.io.SequenceInputStream;
-import java.net.URI;
-import java.security.Principal;
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Objects;
-import java.util.Optional;
-
-import static com.yahoo.yolean.Exceptions.uncheck;
-import static java.util.Objects.requireNonNullElse;
-
-/**
- * This provides read and write operations for the audit log.
- *
- * @author mpolden
- */
-public class AuditLogger {
-
- /** The TTL of log entries. Entries older than this will be removed when the log is updated */
- private static final Duration entryTtl = Duration.ofDays(14);
- private static final int maxEntries = 2000;
-
- private final CuratorDb db;
- private final Clock clock;
-
- public AuditLogger(CuratorDb db, Clock clock) {
- this.db = Objects.requireNonNull(db, "db must be non-null");
- this.clock = Objects.requireNonNull(clock, "clock must be non-null");
- }
-
- /** Read the current audit log */
- public AuditLog readLog() {
- return db.readAuditLog();
- }
-
- /**
- * Write a log entry for given request to the audit log.
- *
- * Note that data contained in the given request may be consumed. Callers should use the returned HttpRequest for
- * further processing.
- */
- public HttpRequest log(HttpRequest request) {
- Optional<AuditLog.Entry.Method> method = auditableMethod(request);
- if (method.isEmpty()) return request; // Nothing to audit, e.g. a GET request
-
- Principal principal = request.getJDiscRequest().getUserPrincipal();
- if (principal == null) {
- throw new IllegalStateException("Cannot audit " + request.getMethod() + " " + request.getUri() +
- " as no principal was found in the request. This is likely caused by a " +
- "misconfiguration and should not happen");
- }
-
- InputStream requestData = requireNonNullElse(request.getData(), InputStream.nullInputStream());
- byte[] data = uncheck(() -> requestData.readNBytes(Entry.maxDataLength));
-
- AuditLog.Entry.Client client = parseClient(request);
- Instant now = clock.instant();
- AuditLog.Entry entry = new AuditLog.Entry(now, client, principal.getName(), method.get(),
- pathAndQueryOf(request.getUri()), data);
- try (Mutex lock = db.lockAuditLog()) {
- AuditLog auditLog = db.readAuditLog()
- .pruneBefore(now.minus(entryTtl))
- .with(entry)
- .first(maxEntries);
- db.writeAuditLog(auditLog);
- }
-
- // Create a new input stream to allow callers to consume request body
- return new HttpRequest(request.getJDiscRequest(),
- new SequenceInputStream(new ByteArrayInputStream(data), requestData),
- request.propertyMap());
- }
-
- private static AuditLog.Entry.Client parseClient(HttpRequest request) {
- String userAgent = request.getHeader(HttpHeaders.Names.USER_AGENT);
- if (userAgent != null) {
- if (userAgent.startsWith("Vespa CLI/")) {
- return AuditLog.Entry.Client.cli;
- } else if (userAgent.startsWith("Vespa Hosted Client ")) {
- return AuditLog.Entry.Client.hv;
- }
- }
- if (request.getPort() == 443) {
- return AuditLog.Entry.Client.console;
- }
- return AuditLog.Entry.Client.other;
- }
-
- /** Returns the auditable method of given request, if any */
- private static Optional<AuditLog.Entry.Method> auditableMethod(HttpRequest request) {
- try {
- return Optional.of(AuditLog.Entry.Method.valueOf(request.getMethod().name()));
- } catch (IllegalArgumentException e) {
- return Optional.empty();
- }
- }
-
- private static String pathAndQueryOf(URI url) {
- String pathAndQuery = url.getPath();
- String query = url.getQuery();
- if (query != null) {
- pathAndQuery += "?" + query;
- }
- return pathAndQuery;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLoggingRequestHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLoggingRequestHandler.java
deleted file mode 100644
index d73b5ef1d15..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLoggingRequestHandler.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.auditlog;
-
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.jdisc.handler.ContentChannel;
-
-/**
- * A handler that logs requests to the audit log. Handlers that need audit logging should extend this and implement
- * {@link AuditLoggingRequestHandler#auditAndHandle(HttpRequest)}.
- *
- * @author mpolden
- */
-public abstract class AuditLoggingRequestHandler extends ThreadedHttpRequestHandler {
-
- private final AuditLogger auditLogger;
-
- public AuditLoggingRequestHandler(Context ctx, AuditLogger auditLogger) {
- super(ctx);
- this.auditLogger = auditLogger;
- }
-
- @Override
- public final HttpResponse handle(HttpRequest request) {
- return auditAndHandle(auditLogger.log(request));
- }
-
- @Override
- public final HttpResponse handle(HttpRequest request, ContentChannel channel) {
- return super.handle(request, channel);
- }
-
- public abstract HttpResponse auditAndHandle(HttpRequest request);
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/package-info.java
deleted file mode 100644
index daafbf7c767..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/auditlog/package-info.java
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author mpolden
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.auditlog;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/AssignedCertificate.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/AssignedCertificate.java
deleted file mode 100644
index 49e2dc5bb0d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/AssignedCertificate.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.certificate;
-
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-
-import java.util.Optional;
-
-/**
- * Represents a certificate and its owner. A certificate is either assigned to all instances of an application, or a
- * specific one.
- *
- * @author mpolden
- */
-public record AssignedCertificate(TenantAndApplicationId application,
- Optional<InstanceName> instance,
- EndpointCertificate certificate,
- boolean shouldValidate) {
-
- public AssignedCertificate with(EndpointCertificate certificate) {
- return new AssignedCertificate(application, instance, certificate, shouldValidate);
- }
-
- public AssignedCertificate withoutInstance() {
- return new AssignedCertificate(application, Optional.empty(), certificate, shouldValidate);
- }
-
- public AssignedCertificate withShouldValidate(boolean shouldValidate) {
- return new AssignedCertificate(application, instance, certificate, shouldValidate);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java
deleted file mode 100644
index 391c9806f0a..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificates.java
+++ /dev/null
@@ -1,318 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.certificate;
-
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.text.Text;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.transaction.NestedTransaction;
-import com.yahoo.vespa.flags.BooleanFlag;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.flags.StringFlag;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.GcpSecretStore;
-import com.yahoo.vespa.hosted.controller.application.GeneratedEndpoint;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.routing.EndpointConfig;
-
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Comparator;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.vespa.hosted.controller.certificate.UnassignedCertificate.State;
-
-/**
- * This provisions, assigns and updates the certificate for a given deployment.
- *
- * See also {@link com.yahoo.vespa.hosted.controller.maintenance.EndpointCertificateMaintainer}, which handles
- * refreshes, deletions and triggers deployments.
- *
- * @author andreer
- * @author mpolden
- */
-public class EndpointCertificates {
-
- private static final Logger LOG = Logger.getLogger(EndpointCertificates.class.getName());
- private static final Duration GCP_CERTIFICATE_EXPIRY_TIME = Duration.ofDays(100); // 100 days, 10 more than notAfter time
-
- private final Controller controller;
- private final CuratorDb curator;
- private final Clock clock;
- private final EndpointCertificateProvider certificateProvider;
- private final EndpointCertificateValidator certificateValidator;
- private final BooleanFlag useAlternateCertProvider;
- private final StringFlag endpointCertificateAlgo;
-
- public EndpointCertificates(Controller controller, EndpointCertificateProvider certificateProvider,
- EndpointCertificateValidator certificateValidator) {
- this.controller = controller;
- this.useAlternateCertProvider = PermanentFlags.USE_ALTERNATIVE_ENDPOINT_CERTIFICATE_PROVIDER.bindTo(controller.flagSource());
- this.endpointCertificateAlgo = PermanentFlags.ENDPOINT_CERTIFICATE_ALGORITHM.bindTo(controller.flagSource());
- this.curator = controller.curator();
- this.clock = controller.clock();
- this.certificateProvider = certificateProvider;
- this.certificateValidator = certificateValidator;
- }
-
- /** Returns a suitable certificate for endpoints of given deployment */
- public EndpointCertificate get(DeploymentId deployment, DeploymentSpec deploymentSpec, Mutex applicationLock) {
- Objects.requireNonNull(applicationLock);
- Instant start = clock.instant();
- EndpointConfig config = controller.routing().endpointConfig(deployment.applicationId());
- EndpointCertificate certificate = assignTo(deployment, deploymentSpec, config);
- Duration duration = Duration.between(start, clock.instant());
- if (duration.toSeconds() > 30) {
- LOG.log(Level.INFO, Text.format("Getting endpoint certificate for %s took %d seconds!", deployment.applicationId().serializedForm(), duration.toSeconds()));
- }
- if (isGcp(deployment)) {
- // This is needed until CKMS is available from GCP
- return validateGcpCertificate(deployment, deploymentSpec, certificate, config);
- }
- return certificate;
- }
-
- private boolean isGcp(DeploymentId deployment) {
- return controller.zoneRegistry().zones().all().in(CloudName.GCP).ids().contains(deployment.zoneId());
- }
-
- private EndpointCertificate validateGcpCertificate(DeploymentId deployment, DeploymentSpec deploymentSpec, EndpointCertificate certificate, EndpointConfig config) {
- // Validate before copying cert to GCP. This will ensure we don't bug out on the first deployment, but will take more time
- List<String> dnsNames = controller.routing().certificateDnsNames(deployment, deploymentSpec, certificate.generatedId().get(), config.supportsLegacy());
- certificateValidator.validate(certificate, deployment.applicationId().serializedForm(), deployment.zoneId(), dnsNames);
- GcpSecretStore gcpSecretStore = controller.serviceRegistry().gcpSecretStore();
- String mangledCertName = "endpointCert_" + certificate.certName().replace('.', '_') + "-v" + certificate.version(); // Google cloud does not accept dots in secrets, but they accept underscores
- String mangledKeyName = "endpointCert_" + certificate.keyName().replace('.', '_') + "-v" + certificate.version(); // Google cloud does not accept dots in secrets, but they accept underscores
- if (gcpSecretStore.getLatestSecretVersion(mangledCertName) == null) {
- gcpSecretStore.setSecret(mangledCertName,
- Optional.of(GCP_CERTIFICATE_EXPIRY_TIME),
- "endpoint-cert-accessor");
- gcpSecretStore.addSecretVersion(mangledCertName,
- controller.secretStore().getSecret(certificate.certName(), certificate.version()));
- }
- if (gcpSecretStore.getLatestSecretVersion(mangledKeyName) == null) {
- gcpSecretStore.setSecret(mangledKeyName,
- Optional.of(GCP_CERTIFICATE_EXPIRY_TIME),
- "endpoint-cert-accessor");
- gcpSecretStore.addSecretVersion(mangledKeyName,
- controller.secretStore().getSecret(certificate.keyName(), certificate.version()));
- }
- return certificate.withVersion(1).withKeyName(mangledKeyName).withCertName(mangledCertName);
- }
-
- private AssignedCertificate assignFromPool(TenantAndApplicationId application, Optional<InstanceName> instanceName, ZoneId zone) {
- try (Mutex lock = controller.curator().lockCertificatePool()) {
- Optional<UnassignedCertificate> candidate = curator.readUnassignedCertificates().stream()
- .filter(pc -> pc.state() == State.ready)
- .min(Comparator.comparingLong(pc -> pc.certificate().lastRequested()));
- if (candidate.isEmpty()) {
- throw new IllegalArgumentException("No endpoint certificate available in pool, for deployment of " +
- application + instanceName.map(i -> "." + i.value()).orElse("")
- + " in " + zone);
- }
- try (NestedTransaction transaction = new NestedTransaction()) {
- curator.removeUnassignedCertificate(candidate.get(), transaction);
- AssignedCertificate assigned = new AssignedCertificate(application, instanceName, candidate.get().certificate(), false);
- curator.writeAssignedCertificate(assigned, transaction);
- transaction.commit();
- return assigned;
- }
- }
- }
-
- private AssignedCertificate instanceLevelCertificate(DeploymentId deployment, DeploymentSpec deploymentSpec, boolean allowPool) {
- TenantAndApplicationId application = TenantAndApplicationId.from(deployment.applicationId());
- Optional<InstanceName> instance = Optional.of(deployment.applicationId().instance());
- Optional<AssignedCertificate> currentCertificate = curator.readAssignedCertificate(application, instance);
- final AssignedCertificate assignedCertificate;
- if (currentCertificate.isEmpty()) {
- Optional<String> generatedId = Optional.empty();
- // Re-use the generated ID contained in an existing certificate (matching this application, this instance,
- // or any other instance present in deployment sec), if any. If this exists we provision a new certificate
- // containing the same ID
- if (!deployment.zoneId().environment().isManuallyDeployed()) {
- generatedId = curator.readAssignedCertificates().stream()
- .filter(ac -> {
- boolean matchingInstance = ac.instance().isPresent() &&
- deploymentSpec.instance(ac.instance().get()).isPresent();
- return (matchingInstance || ac.instance().isEmpty()) &&
- ac.application().equals(application);
- })
- .map(AssignedCertificate::certificate)
- .flatMap(ac -> ac.generatedId().stream())
- .findFirst();
- }
- if (allowPool && generatedId.isEmpty()) {
- assignedCertificate = assignFromPool(application, instance, deployment.zoneId());
- } else {
- if (generatedId.isEmpty()) {
- generatedId = Optional.of(generateId());
- }
- EndpointCertificate provisionedCertificate = provision(deployment, Optional.empty(), deploymentSpec, generatedId.get());
- // We do not validate the certificate if one has never existed before - because we do not want to
- // wait for it to be available before we deploy. This allows the config server to start
- // provisioning nodes ASAP, and the risk is small for a new deployment.
- assignedCertificate = new AssignedCertificate(application, instance, provisionedCertificate, false);
- }
- } else {
- assignedCertificate = currentCertificate.get().withShouldValidate(!allowPool);
- }
- return assignedCertificate;
- }
-
- private AssignedCertificate applicationLevelCertificate(DeploymentId deployment) {
- if (deployment.zoneId().environment().isManuallyDeployed()) {
- throw new IllegalArgumentException(deployment + " is manually deployed and cannot assign an application-level certificate");
- }
- TenantAndApplicationId application = TenantAndApplicationId.from(deployment.applicationId());
- Optional<AssignedCertificate> applicationLevelCertificate = curator.readAssignedCertificate(application, Optional.empty());
- if (applicationLevelCertificate.isEmpty()) {
- Optional<AssignedCertificate> instanceLevelCertificate = curator.readAssignedCertificate(application, Optional.of(deployment.applicationId().instance()));
- // Migrate from instance-level certificate
- if (instanceLevelCertificate.isPresent()) {
- try (var transaction = new NestedTransaction()) {
- AssignedCertificate assignedCertificate = instanceLevelCertificate.get().withoutInstance();
- curator.removeAssignedCertificate(application, Optional.of(deployment.applicationId().instance()), transaction);
- curator.writeAssignedCertificate(assignedCertificate, transaction);
- transaction.commit();
- return assignedCertificate;
- }
- } else {
- return assignFromPool(application, Optional.empty(), deployment.zoneId());
- }
- }
- return applicationLevelCertificate.get();
- }
-
- /** Assign a certificate to given deployment. A new certificate is provisioned (possibly from a pool) and reconfigured as necessary */
- private EndpointCertificate assignTo(DeploymentId deployment, DeploymentSpec deploymentSpec, EndpointConfig config) {
- // Assign certificate based on endpoint config
- AssignedCertificate assignedCertificate = switch (config) {
- case legacy, combined -> instanceLevelCertificate(deployment, deploymentSpec, false);
- case generated -> deployment.zoneId().environment().isManuallyDeployed()
- ? instanceLevelCertificate(deployment, deploymentSpec, true)
- : applicationLevelCertificate(deployment);
- };
-
- // Generate ID if not already present in certificate
- Optional<String> generatedId = assignedCertificate.certificate().generatedId();
- if (generatedId.isEmpty()) {
- generatedId = Optional.of(generateId());
- assignedCertificate = assignedCertificate.with(assignedCertificate.certificate().withGeneratedId(generatedId.get()));
- }
-
- // Ensure all wanted names are present in certificate
- List<String> wantedNames = controller.routing().certificateDnsNames(deployment, deploymentSpec, generatedId.get(), config.supportsLegacy());
- Set<String> currentNames = Set.copyOf(assignedCertificate.certificate().requestedDnsSans());
- // TODO(mpolden): Consider requiring exact match for generated as we likely want to remove any legacy names in this case
- if (!currentNames.containsAll(wantedNames)) {
- EndpointCertificate updatedCertificate = provision(deployment, Optional.of(assignedCertificate.certificate()), deploymentSpec, generatedId.get());
- // Validation is unlikely to succeed in this case, as certificate must be available first. Controller will retry
- assignedCertificate = assignedCertificate.with(updatedCertificate)
- .withShouldValidate(true);
- }
-
- // Require that generated ID is always set, for any kind of certificate
- if (assignedCertificate.certificate().generatedId().isEmpty()) {
- throw new IllegalArgumentException("Certificate for " + deployment + " does not contain generated ID: " +
- assignedCertificate.certificate());
- }
-
- // Update the time we last requested this certificate. This field is used by EndpointCertificateMaintainer to
- // determine stale certificates
- assignedCertificate = assignedCertificate.with(assignedCertificate.certificate().withLastRequested(clock.instant().getEpochSecond()));
- curator.writeAssignedCertificate(assignedCertificate);
-
- // Validate if we're re-assigned an existing certificate, or if we updated the names of an existing certificate
- if (assignedCertificate.shouldValidate()) {
- certificateValidator.validate(assignedCertificate.certificate(), deployment.applicationId().serializedForm(),
- deployment.zoneId(), wantedNames);
- }
-
- return assignedCertificate.certificate();
- }
-
- private String generateId() {
- List<String> unassignedIds = curator.readUnassignedCertificates().stream()
- .map(UnassignedCertificate::id)
- .toList();
- List<String> assignedIds = curator.readAssignedCertificates().stream()
- .map(AssignedCertificate::certificate)
- .map(EndpointCertificate::generatedId)
- .flatMap(Optional::stream)
- .toList();
- Set<String> allIds = Stream.concat(unassignedIds.stream(), assignedIds.stream()).collect(Collectors.toSet());
- String id;
- do {
- id = GeneratedEndpoint.createPart(controller.random(true));
- } while (allIds.contains(id));
- return id;
- }
-
- private EndpointCertificate provision(DeploymentId deployment,
- Optional<EndpointCertificate> current,
- DeploymentSpec deploymentSpec,
- String generatedId) {
- List<ZoneId> zonesInSystem = controller.zoneRegistry().zones().controllerUpgraded().ids();
- Set<ZoneId> requiredZones = new LinkedHashSet<>();
- requiredZones.add(deployment.zoneId());
- if (!deployment.zoneId().environment().isManuallyDeployed()) {
- // If not deploying to a dev or perf zone, require all prod zones in deployment spec + test and staging
- Optional<DeploymentInstanceSpec> instanceSpec = deploymentSpec.instance(deployment.applicationId().instance());
- zonesInSystem.stream()
- .filter(zone -> zone.environment().isTest() ||
- (instanceSpec.isPresent() &&
- instanceSpec.get().deploysTo(zone.environment(), zone.region())))
- .forEach(requiredZones::add);
- }
- Set<String> wantedNames = requiredZones.stream()
- .flatMap(zone -> controller.routing().certificateDnsNames(new DeploymentId(deployment.applicationId(), zone),
- deploymentSpec, generatedId, true)
- .stream())
- .collect(Collectors.toCollection(LinkedHashSet::new));
-
- // Preserve any currently present names that are still valid (i.e. the name points to a zone found in this system)
- Set<String> currentNames = current.map(EndpointCertificate::requestedDnsSans)
- .map(Set::copyOf)
- .orElseGet(Set::of);
- for (var zone : zonesInSystem) {
- List<String> wantedNamesZone = controller.routing().certificateDnsNames(new DeploymentId(deployment.applicationId(), zone),
- deploymentSpec,
- generatedId,
- true);
- if (currentNames.containsAll(wantedNamesZone)) {
- wantedNames.addAll(wantedNamesZone);
- }
- }
-
- // Request certificate
- LOG.log(Level.INFO, String.format("Requesting new endpoint certificate for application %s", deployment.applicationId().serializedForm()));
- String algo = endpointCertificateAlgo.with(FetchVector.Dimension.INSTANCE_ID, deployment.applicationId().serializedForm()).value();
- boolean useAlternativeProvider = useAlternateCertProvider.with(FetchVector.Dimension.INSTANCE_ID, deployment.applicationId().serializedForm()).value();
- String keyPrefix = deployment.applicationId().toFullString();
- Instant t0 = controller.clock().instant();
- EndpointCertificate endpointCertificate = certificateProvider.requestCaSignedCertificate(keyPrefix, List.copyOf(wantedNames), current, algo, useAlternativeProvider);
- Instant t1 = controller.clock().instant();
- LOG.log(Level.INFO, String.format("Endpoint certificate request for application %s returned after %s", deployment.applicationId().serializedForm(), Duration.between(t0, t1)));
- return endpointCertificate.withGeneratedId(generatedId);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/UnassignedCertificate.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/UnassignedCertificate.java
deleted file mode 100644
index 1d1f4938758..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/certificate/UnassignedCertificate.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.certificate;
-
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-
-/**
- * An unassigned certificate, which exists in a pre-provisioned pool of certificates. Once assigned to an application,
- * the certificate is removed from the pool.
- *
- * @param certificate Details of the certificate
- * @param state Current state of this
- *
- * @author andreer
- */
-public record UnassignedCertificate(EndpointCertificate certificate, UnassignedCertificate.State state) {
-
- public UnassignedCertificate {
- if (certificate.generatedId().isEmpty()) {
- throw new IllegalArgumentException("generatedId must be set for a pooled certificate");
- }
- }
-
- public String id() {
- return certificate.generatedId().get();
- }
-
- public UnassignedCertificate withState(State state) {
- return new UnassignedCertificate(certificate, state);
- }
-
- public enum State {
- /** The certificate is ready for assignment */
- ready,
-
- /** The certificate is requested and is being provisioned */
- requested
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/concurrent/Once.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/concurrent/Once.java
deleted file mode 100644
index 2e717f16d0e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/concurrent/Once.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.concurrent;
-
-import java.time.Duration;
-import java.util.Objects;
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * Execute a runnable exactly once in a background thread.
- *
- * @author mpolden
- */
-public class Once extends TimerTask {
-
- private static final Logger log = Logger.getLogger(Once.class.getName());
-
- private final Runnable runnable;
- private final Timer timer = new Timer(true);
-
- // private to avoid exposing run method
- private Once(Runnable runnable, Duration delay) {
- this.runnable = Objects.requireNonNull(runnable, "runnable must be non-null");
- Objects.requireNonNull(delay, "delay must be non-null");
- timer.schedule(this, delay.toMillis());
- }
-
- /** Execute runnable after given delay */
- public static void after(Duration delay, Runnable runnable) {
- new Once(runnable, delay);
- }
-
- @Override
- public void run() {
- try {
- runnable.run();
- } catch (Throwable t) {
- log.log(Level.WARNING, "Task '" + runnable + "' failed", t);
- } finally {
- timer.cancel();
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/ConvergenceSummary.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/ConvergenceSummary.java
deleted file mode 100644
index 2b1d00ada95..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/ConvergenceSummary.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import java.util.Objects;
-
-/**
- * Summary of node and service status during a deployment job.
- *
- * @author jonmv
- */
-public class ConvergenceSummary {
-
- private final long nodes;
- private final long down;
- private final long upgradingOs;
- private final long upgradingFirmware;
- private final long needPlatformUpgrade;
- private final long upgradingPlatform;
- private final long needReboot;
- private final long rebooting;
- private final long needRestart;
- private final long restarting;
- private final long services;
- private final long needNewConfig;
- private final long retiring;
-
- public ConvergenceSummary(long nodes, long down, long upgradingOs, long upgradingFirmware, long needPlatformUpgrade, long upgradingPlatform,
- long needReboot, long rebooting, long needRestart, long restarting, long services, long needNewConfig, long retiring) {
- this.nodes = nodes;
- this.down = down;
- this.upgradingOs = upgradingOs;
- this.upgradingFirmware = upgradingFirmware;
- this.needPlatformUpgrade = needPlatformUpgrade;
- this.upgradingPlatform = upgradingPlatform;
- this.needReboot = needReboot;
- this.rebooting = rebooting;
- this.needRestart = needRestart;
- this.restarting = restarting;
- this.services = services;
- this.needNewConfig = needNewConfig;
- this.retiring = retiring;
- }
-
- /** Number of nodes in the application. */
- public long nodes() {
- return nodes;
- }
-
- /** Number of nodes allowed to be down. */
- public long down() {
- return down;
- }
-
- /** Number of nodes down for OS upgrade. */
- public long upgradingOs() {
- return upgradingOs;
- }
-
- /** Number of nodes down for firmware upgrade. */
- public long upgradingFirmware() {
- return upgradingFirmware;
- }
-
- /** Number of nodes in need of a platform upgrade. */
- public long needPlatformUpgrade() {
- return needPlatformUpgrade;
- }
-
- /** Number of nodes down for platform upgrade. */
- public long upgradingPlatform() {
- return upgradingPlatform;
- }
-
- /** Number of nodes in need of a reboot. */
- public long needReboot() {
- return needReboot;
- }
-
- /** Number of nodes down for reboot. */
- public long rebooting() {
- return rebooting;
- }
-
- /** Number of nodes in need of a restart. */
- public long needRestart() {
- return needRestart;
- }
-
- /** Number of nodes down for restart. */
- public long restarting() {
- return restarting;
- }
-
- /** Number of services in the application. */
- public long services() {
- return services;
- }
-
- /** Number of services with outdated config generation. */
- public long needNewConfig() {
- return needNewConfig;
- }
-
- /** Number of nodes that are retiring. */
- public long retiring() {
- return retiring;
- }
-
- /** Whether the convergence is done. */
- public boolean converged() {
- return nodes > 0
- && needPlatformUpgrade == 0
- && needReboot == 0
- && needRestart == 0
- && services > 0
- && needNewConfig == 0;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ConvergenceSummary that = (ConvergenceSummary) o;
- return nodes == that.nodes &&
- down == that.down &&
- upgradingOs == that.upgradingOs &&
- upgradingFirmware == that.upgradingFirmware &&
- needPlatformUpgrade == that.needPlatformUpgrade &&
- upgradingPlatform == that.upgradingPlatform &&
- needReboot == that.needReboot &&
- rebooting == that.rebooting &&
- needRestart == that.needRestart &&
- restarting == that.restarting &&
- services == that.services &&
- needNewConfig == that.needNewConfig &&
- retiring == that.retiring;
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(nodes, down, upgradingOs, upgradingFirmware, needPlatformUpgrade, upgradingPlatform, needReboot, rebooting, needRestart, restarting, services, needNewConfig, retiring);
- }
-
-}
-
-
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
deleted file mode 100644
index 223ba546b3e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatus.java
+++ /dev/null
@@ -1,1285 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.google.common.collect.ImmutableMap;
-import com.yahoo.component.Version;
-import com.yahoo.component.VersionCompatibility;
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.DeploymentSpec.DeclaredTest;
-import com.yahoo.config.application.api.DeploymentSpec.DeclaredZone;
-import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy;
-import com.yahoo.config.application.api.DeploymentSpec.UpgradeRollout;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.stream.CustomCollectors;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.deployment.Run.Reason;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.collections.Iterables.reversed;
-import static com.yahoo.config.application.api.DeploymentSpec.RevisionTarget.next;
-import static com.yahoo.config.provision.Environment.prod;
-import static com.yahoo.config.provision.Environment.staging;
-import static com.yahoo.config.provision.Environment.test;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.cancelled;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.invalidApplication;
-import static java.util.Comparator.comparing;
-import static java.util.Comparator.naturalOrder;
-import static java.util.Comparator.reverseOrder;
-import static java.util.Objects.requireNonNull;
-import static java.util.function.BinaryOperator.maxBy;
-import static java.util.stream.Collectors.collectingAndThen;
-import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.mapping;
-import static java.util.stream.Collectors.toMap;
-import static java.util.stream.Collectors.toSet;
-
-/**
- * Status of the deployment jobs of an {@link Application}.
- *
- * @author jonmv
- */
-public class DeploymentStatus {
-
- private static <T> List<T> union(List<T> first, List<T> second) {
- return Stream.concat(first.stream(), second.stream()).distinct().toList();
- }
-
- private final Application application;
- private final JobList allJobs;
- private final VersionStatus versionStatus;
- private final Version systemVersion;
- private final Function<InstanceName, VersionCompatibility> versionCompatibility;
- private final ZoneRegistry zones;
- private final Instant now;
- private final Map<JobId, StepStatus> jobSteps;
- private final List<StepStatus> allSteps;
-
- public DeploymentStatus(Application application, Function<JobId, JobStatus> allJobs, ZoneRegistry zones, VersionStatus versionStatus,
- Version systemVersion, Function<InstanceName, VersionCompatibility> versionCompatibility, Instant now) {
- this.application = requireNonNull(application);
- this.zones = zones;
- this.versionStatus = requireNonNull(versionStatus);
- this.systemVersion = requireNonNull(systemVersion);
- this.versionCompatibility = versionCompatibility;
- this.now = requireNonNull(now);
- List<StepStatus> allSteps = new ArrayList<>();
- Map<JobId, JobStatus> jobs = new HashMap<>();
- this.jobSteps = jobDependencies(application.deploymentSpec(), allSteps, job -> jobs.computeIfAbsent(job, allJobs));
- this.allSteps = Collections.unmodifiableList(allSteps);
- this.allJobs = JobList.from(jobSteps.keySet().stream().map(allJobs).toList());
- }
-
- private JobType systemTest(JobType dependent) {
- return JobType.systemTest(zones, dependent == null ? null : findCloud(dependent));
- }
-
- private JobType stagingTest(JobType dependent) {
- return JobType.stagingTest(zones, dependent == null ? null : findCloud(dependent));
- }
-
- /** The application this deployment status concerns. */
- public Application application() {
- return application;
- }
-
- /** A filterable list of the status of all jobs for this application. */
- public JobList jobs() {
- return allJobs;
- }
-
- /** Whether any jobs both dependent on the dependency, and a dependency for the dependent, are failing. */
- private boolean hasFailures(StepStatus dependency, StepStatus dependent) {
- Set<StepStatus> dependents = new HashSet<>();
- fillDependents(dependency, new HashSet<>(), dependents, dependent);
- Set<JobId> criticalJobs = dependents.stream().flatMap(step -> step.job().stream()).collect(toSet());
-
- return ! allJobs.matching(job -> criticalJobs.contains(job.id()))
- .failingHard()
- .isEmpty();
- }
-
- private boolean fillDependents(StepStatus dependency, Set<StepStatus> visited, Set<StepStatus> dependents, StepStatus current) {
- if (visited.contains(current))
- return dependents.contains(current);
-
- if (dependency == current)
- dependents.add(current);
- else
- for (StepStatus dep : current.dependencies)
- if (fillDependents(dependency, visited, dependents, dep))
- dependents.add(current);
-
- visited.add(current);
- return dependents.contains(current);
- }
-
- /** Whether any job is failing on versions selected by the given filter, with errors other than lack of capacity in a test zone.. */
- public boolean hasFailures(Predicate<RevisionId> revisionFilter) {
- return ! allJobs.failingHard()
- .matching(job -> revisionFilter.test(job.lastTriggered().get().versions().targetRevision()))
- .isEmpty();
- }
-
- /** Whether any jobs of this application are failing with other errors than lack of capacity in a test zone. */
- public boolean hasFailures() {
- return ! allJobs.failingHard().isEmpty();
- }
-
- /** All job statuses, by job type, for the given instance. */
- public Map<JobType, JobStatus> instanceJobs(InstanceName instance) {
- return allJobs.asList().stream()
- .filter(job -> job.id().application().equals(application.id().instance(instance)))
- .collect(CustomCollectors.toLinkedMap(job -> job.id().type(), Function.identity()));
- }
-
- /** Filterable job status lists for each instance of this application. */
- public Map<ApplicationId, JobList> instanceJobs() {
- return allJobs.groupingBy(job -> job.id().application());
- }
-
- /** Returns change potentially with a compatibility platform added, if required for the change to roll out to the given instance. */
- public Change withPermittedPlatform(Change change, InstanceName instance, boolean allowOutdatedPlatform) {
- Change augmented = withCompatibilityPlatform(change, instance);
- if (allowOutdatedPlatform)
- return augmented;
-
- // If compatibility platform is present, require that jobs have previously been run on that platform's major.
- // If platform is not present, app is already on the (old) platform iff. it has production deployments.
- boolean alreadyDeployedOnPlatform = augmented.platform().map(platform -> allJobs.production().not().test().asList().stream()
- .anyMatch(job -> job.runs().values().stream()
- .anyMatch(run -> run.versions().targetPlatform().getMajor() == platform.getMajor())))
- .orElse( ! application.productionDeployments().values().stream().allMatch(List::isEmpty));
-
- // Verify target platform is either current, or was previously deployed for this app.
- if (augmented.platform().isPresent() && ! versionStatus.isOnCurrentMajor(augmented.platform().get()) && ! alreadyDeployedOnPlatform)
- throw new IllegalArgumentException("platform version " + augmented.platform().get() + " is not on a current major version in this system");
-
- Version latestHighConfidencePlatform = null;
- for (VespaVersion platform : versionStatus.deployableVersions())
- if (platform.confidence().equalOrHigherThan(Confidence.high))
- latestHighConfidencePlatform = platform.versionNumber();
-
- // Verify package is compatible with the current major, or newer, or that there already are deployments on a compatible, outdated platform.
- if (latestHighConfidencePlatform != null) {
- Version target = latestHighConfidencePlatform;
- augmented.revision().flatMap(revision -> application.revisions().get(revision).compileVersion())
- .filter(target::isAfter)
- .ifPresent(compiled -> {
- if (versionCompatibility.apply(instance).refuse(target, compiled) && ! alreadyDeployedOnPlatform)
- throw new IllegalArgumentException("compile version " + compiled + " is incompatible with the current major version of this system");
- });
- }
-
- return augmented;
- }
-
- private Change withCompatibilityPlatform(Change change, InstanceName instance) {
- if (change.revision().isEmpty())
- return change;
-
- Optional<Version> compileVersion = change.revision()
- .map(application.revisions()::get)
- .flatMap(ApplicationVersion::compileVersion);
-
- // If the revision requires a certain platform for compatibility, add that here, unless we're already deploying a compatible platform.
- VersionCompatibility compatibility = versionCompatibility.apply(instance);
- Predicate<Version> compatibleWithCompileVersion = version -> compileVersion.map(compiled -> compatibility.accept(version, compiled)).orElse(true);
- if (change.platform().map(compatibleWithCompileVersion::test).orElse(false))
- return change;
-
- if ( application.productionDeployments().isEmpty()
- || application.productionDeployments().getOrDefault(instance, List.of()).stream()
- .anyMatch(deployment -> ! compatibleWithCompileVersion.test(deployment.version()))) {
- for (Version platform : targetsForPolicy(versionStatus, systemVersion, application.deploymentSpec().requireInstance(instance).upgradePolicy()))
- if (compatibleWithCompileVersion.test(platform))
- return change.withoutPlatformPin().with(platform);
- }
- return change;
- }
-
- /** Returns target versions for given confidence, by descending version number. */
- public static List<Version> targetsForPolicy(VersionStatus versions, Version systemVersion, DeploymentSpec.UpgradePolicy policy) {
- if (policy == DeploymentSpec.UpgradePolicy.canary)
- return List.of(systemVersion);
-
- VespaVersion.Confidence target = policy == DeploymentSpec.UpgradePolicy.defaultPolicy ? VespaVersion.Confidence.normal : VespaVersion.Confidence.high;
- return versions.deployableVersions().stream()
- .filter(version -> version.confidence().equalOrHigherThan(target))
- .map(VespaVersion::versionNumber)
- .sorted(reverseOrder())
- .toList();
- }
-
-
- /**
- * The set of jobs that need to run for the changes of each instance of the application to be considered complete,
- * and any test jobs for any outstanding change, which will likely be needed to later deploy this change.
- */
- public Map<JobId, List<Job>> jobsToRun() {
- if (application.revisions().last().isEmpty()) return Map.of();
-
- Map<InstanceName, Change> changes = new LinkedHashMap<>();
- for (InstanceName instance : application.deploymentSpec().instanceNames())
- changes.put(instance, application.require(instance).change());
- Map<JobId, List<Job>> jobs = jobsToRun(changes);
-
- // Add test jobs for any outstanding change.
- Map<InstanceName, Change> outstandingChanges = new LinkedHashMap<>();
- for (InstanceName instance : application.deploymentSpec().instanceNames()) {
- Change outstanding = outstandingChange(instance);
- if (outstanding.hasTargets())
- outstandingChanges.put(instance, outstanding.onTopOf(application.require(instance).change().withoutRevisionPin()));
- }
- var testJobs = jobsToRun(outstandingChanges, true).entrySet().stream()
- .filter(entry -> ! entry.getKey().type().isProduction());
-
- return Stream.concat(jobs.entrySet().stream(), testJobs)
- .collect(collectingAndThen(toMap(Map.Entry::getKey,
- Map.Entry::getValue,
- DeploymentStatus::union,
- LinkedHashMap::new),
- Collections::unmodifiableMap));
- }
-
- private Map<JobId, List<Job>> jobsToRun(Map<InstanceName, Change> changes, boolean eagerTests) {
- if (application.revisions().last().isEmpty()) return Map.of();
-
- Map<JobId, List<Job>> productionJobs = new LinkedHashMap<>();
- changes.forEach((instance, change) -> productionJobs.putAll(productionJobs(instance, change, eagerTests)));
- Map<JobId, List<Job>> testJobs = testJobs(productionJobs);
- Map<JobId, List<Job>> jobs = new LinkedHashMap<>(testJobs);
- jobs.putAll(productionJobs);
- // Add runs for idle, declared test jobs if they have no successes on their instance's change's versions.
- jobSteps.forEach((job, step) -> {
- if ( ! step.isDeclared() || job.type().isProduction() || jobs.containsKey(job))
- return;
-
- Change change = changes.get(job.application().instance());
- if (change == null || ! change.hasTargets())
- return;
-
- Map<CloudName, Optional<JobId>> firstProductionJobsWithDeployment = firstDependentProductionJobsWithDeployment(job.application().instance());
- firstProductionJobsWithDeployment.forEach((cloud, firstProductionJobWithDeploymentInCloud) -> {
- Versions versions = Versions.from(change,
- application,
- firstProductionJobWithDeploymentInCloud.flatMap(this::deploymentFor),
- fallbackPlatform(change, job));
- if (step.completedAt(change, firstProductionJobWithDeploymentInCloud).isEmpty()) {
- JobType typeWithZone = job.type().isSystemTest() ? JobType.systemTest(zones, cloud) : JobType.stagingTest(zones, cloud);
- Readiness readiness = step.readiness(change, firstProductionJobWithDeploymentInCloud);
- jobs.merge(job, List.of(new Job(typeWithZone,
- versions,
- readiness.okAt(now) && jobs().get(job).get().isRunning() ? readiness.running() : readiness,
- change,
- null)), DeploymentStatus::union);
- }
- });
- });
- return Collections.unmodifiableMap(jobs);
- }
-
- /**
- * Returns the clouds, and their first production deployments, that depend on this instance; or,
- * if no such deployments exist, all clouds the application deploy to, and their first production deployments; or
- * if no clouds are deployed to at all, the system default cloud.
- */
- public Map<CloudName, Optional<JobId>> firstDependentProductionJobsWithDeployment(InstanceName testInstance) {
- // Find instances' dependencies on each other: these are topologically ordered, so a simple traversal does it.
- Map<InstanceName, Set<InstanceName>> dependencies = new HashMap<>();
- instanceSteps().forEach((name, step) -> {
- dependencies.put(name, new HashSet<>());
- dependencies.get(name).add(name);
- for (StepStatus dependency : step.dependencies()) {
- dependencies.get(name).add(dependency.instance());
- dependencies.get(name).addAll(dependencies.get(dependency.instance));
- }
- });
-
- Map<CloudName, Optional<JobId>> independentJobsPerCloud = new HashMap<>();
- Map<CloudName, Optional<JobId>> jobsPerCloud = new HashMap<>();
- jobSteps.forEach((job, step) -> {
- if ( ! job.type().isProduction() || ! job.type().isDeployment())
- return;
-
- (dependencies.get(step.instance()).contains(testInstance) ? jobsPerCloud
- : independentJobsPerCloud)
- .merge(findCloud(job.type()),
- Optional.of(job),
- (o, n) -> o.filter(v -> deploymentFor(v).isPresent()) // Keep first if its deployment is present.
- .or(() -> n.filter(v -> deploymentFor(v).isPresent())) // Use next if only its deployment is present.
- .or(() -> o)); // Keep first if none have deployments.
- });
-
- if (jobsPerCloud.isEmpty())
- jobsPerCloud.putAll(independentJobsPerCloud);
-
- if (jobsPerCloud.isEmpty())
- jobsPerCloud.put(zones.systemZone().getCloudName(), Optional.empty());
-
- return jobsPerCloud;
- }
-
-
- /** Fall back to the newest, deployable platform, which is compatible with what we want to deploy. */
- public Supplier<Version> fallbackPlatform(Change change, JobId job) {
- return () -> {
- InstanceName instance = job.application().instance();
- Optional<Version> compileVersion = change.revision().map(application.revisions()::get).flatMap(ApplicationVersion::compileVersion);
- List<Version> targets = targetsForPolicy(versionStatus,
- systemVersion,
- application.deploymentSpec().instance(instance)
- .map(DeploymentInstanceSpec::upgradePolicy)
- .orElse(UpgradePolicy.defaultPolicy));
-
- // Prefer fallback with proper confidence.
- for (Version target : targets)
- if (compileVersion.isEmpty() || versionCompatibility.apply(instance).accept(target, compileVersion.get()))
- return target;
-
- // Try fallback with any confidence.
- for (VespaVersion target : reversed(versionStatus.deployableVersions()))
- if (compileVersion.isEmpty() || versionCompatibility.apply(instance).accept(target.versionNumber(), compileVersion.get()))
- return target.versionNumber();
-
- return compileVersion.orElseThrow(() -> new IllegalArgumentException("no legal platform version exists in this system for compile version " + compileVersion.get()));
- };
- }
-
-
- /** The set of jobs that need to run for the given changes to be considered complete. */
- public boolean hasCompleted(InstanceName instance, Change change) {
- DeploymentInstanceSpec spec = application.deploymentSpec().requireInstance(instance);
- if ((spec.concerns(test) || spec.concerns(staging)) && ! spec.concerns(prod)) {
- if (newestTested(instance, run -> run.versions().targetRevision()).map(change::downgrades).orElse(false)) return true;
- if (newestTested(instance, run -> run.versions().targetPlatform()).map(change::downgrades).orElse(false)) return true;
- }
-
- return jobsToRun(Map.of(instance, change), false).isEmpty();
- }
-
- /** The set of jobs that need to run for the given changes to be considered complete. */
- public Map<JobId, List<Job>> jobsToRun(Map<InstanceName, Change> changes) {
- return jobsToRun(changes, false);
- }
-
- /** The step status for all steps in the deployment spec of this, which are jobs, in the same order as in the deployment spec. */
- public Map<JobId, StepStatus> jobSteps() { return jobSteps; }
-
- public Map<InstanceName, StepStatus> instanceSteps() {
- ImmutableMap.Builder<InstanceName, StepStatus> instances = ImmutableMap.builder();
- for (StepStatus status : allSteps)
- if (status instanceof InstanceStatus)
- instances.put(status.instance(), status);
- return instances.build();
- }
-
- /** The step status for all relevant steps in the deployment spec of this, in the same order as in the deployment spec. */
- public List<StepStatus> allSteps() {
- return allSteps;
- }
-
- public Optional<Deployment> deploymentFor(JobId job) {
- return Optional.ofNullable(application.require(job.application().instance())
- .deployments().get(job.type().zone()));
- }
-
- private <T extends Comparable<T>> Optional<T> newestTested(InstanceName instance, Function<Run, T> runMapper) {
- Set<CloudName> clouds = Stream.concat(Stream.of(zones.systemZone().getCloudName()),
- jobSteps.keySet().stream()
- .filter(job -> job.type().isProduction())
- .map(job -> findCloud(job.type())))
- .collect(toSet());
- List<ZoneId> testZones = new ArrayList<>();
- if (application.deploymentSpec().requireInstance(instance).concerns(test))
- for (CloudName cloud: clouds) testZones.add(JobType.systemTest(zones, cloud).zone());
- if (application.deploymentSpec().requireInstance(instance).concerns(staging))
- for (CloudName cloud: clouds) testZones.add(JobType.stagingTest(zones, cloud).zone());
-
- Map<ZoneId, Optional<T>> newestPerZone = instanceJobs().get(application.id().instance(instance))
- .type(systemTest(null), stagingTest(null))
- .asList().stream().flatMap(jobs -> jobs.runs().values().stream())
- .filter(Run::hasSucceeded)
- .collect(groupingBy(run -> run.id().type().zone(),
- mapping(runMapper, Collectors.maxBy(naturalOrder()))));
- return newestPerZone.keySet().containsAll(testZones)
- ? testZones.stream().map(newestPerZone::get)
- .reduce((o, n) -> o.isEmpty() || n.isEmpty() ? Optional.empty() : n.get().compareTo(o.get()) < 0 ? n : o)
- .orElse(Optional.empty())
- : Optional.empty();
- }
-
- /**
- * The change to a revision which all dependencies of the given instance has completed,
- * which does not downgrade any deployments in the instance,
- * which is not already rolling out to the instance, and
- * which causes at least one job to run if deployed to the instance.
- * For the "next" revision target policy it is the oldest such revision; otherwise, it is the latest.
- */
- public Change outstandingChange(InstanceName instance) {
- StepStatus status = instanceSteps().get(instance);
- if (status == null) return Change.empty();
- DeploymentInstanceSpec spec = application.deploymentSpec().requireInstance(instance);
- boolean ascending = next == spec.revisionTarget();
- int cumulativeRisk = 0;
- int nextRisk = 0;
- int skippedCumulativeRisk = 0;
- Instant readySince = now;
-
- Optional<RevisionId> newestRevision = application.productionDeployments()
- .getOrDefault(instance, List.of()).stream()
- .map(Deployment::revision).max(naturalOrder());
- Change candidate = Change.empty();
- for (ApplicationVersion version : application.revisions().deployable(ascending)) {
- // A revision is only a candidate if it upgrades, and does not downgrade, this instance.
- Change change = Change.of(version.id());
- if ( newestRevision.isPresent() && change.downgrades(newestRevision.get())
- || ! application.require(instance).change().revision().map(change::upgrades).orElse(true)
- || hasCompleted(instance, change)) {
- if (ascending) continue; // Keep looking for the next revision which is an upgrade, or ...
- else return Change.empty(); // ... if the latest is already complete, there's nothing outstanding.
- }
-
- // This revision contains something new, so start aggregating the risk score.
- skippedCumulativeRisk += version.risk();
- nextRisk = nextRisk > 0 ? nextRisk : version.risk();
- // If it's not yet ready to roll out, we keep looking.
- Optional<Instant> readyAt = status.dependenciesCompletedAt(Change.of(version.id()), Optional.empty());
- if (readyAt.map(now::isBefore).orElse(true)) continue;
-
- // It's ready. If looking for the latest, max risk is 0, and we'll return now; otherwise, we _may_ keep on looking for more.
- cumulativeRisk += skippedCumulativeRisk;
- skippedCumulativeRisk = 0;
- nextRisk = 0;
- if (cumulativeRisk >= spec.maxRisk())
- return candidate.equals(Change.empty()) ? change : candidate; // If the first candidate exceeds max risk, we have to accept that.
-
- // Otherwise, we may note this as a candidate, and keep looking for a newer revision, unless that makes us exceed max risk.
- if (readyAt.get().isBefore(readySince)) readySince = readyAt.get();
- candidate = change;
- }
- // If min risk is ready, or max idle time has passed, we return the candidate. Otherwise, no outstanding change is ready.
- return instanceJobs(instance).values().stream().allMatch(jobs -> jobs.lastTriggered().isEmpty())
- || cumulativeRisk >= spec.minRisk()
- || cumulativeRisk + nextRisk > spec.maxRisk()
- || ! now.isBefore(readySince.plus(Duration.ofHours(spec.maxIdleHours())))
- ? candidate : Change.empty();
- }
-
- /** Earliest instant when job was triggered with given versions, or both system and staging tests were successful. */
- public Readiness verifiedAt(JobId job, Versions versions) {
- Readiness triggered = allJobs.get(job)
- .flatMap(status -> status.runs().values().stream()
- .filter(run -> run.versions().equals(versions))
- .findFirst())
- .map(Run::start)
- .map(Readiness::new)
- .orElse(Readiness.unverified);
- Readiness systemTested = testedAt(job, systemTest(job.type()), versions);
- Readiness stagingTested = testedAt(job, stagingTest(job.type()), versions);
- if (! systemTested.ok() || ! stagingTested.ok()) return triggered;
- Readiness tested = min(systemTested, stagingTested);
- return triggered.ok() && triggered.at().isBefore(tested.at) ? triggered : tested;
- }
-
- /** Earliest instant when versions were tested for the given instance. */
- private Readiness testedAt(JobId job, JobType type, Versions versions) {
- return prerequisiteTests(job, type).stream()
- .map(test -> allJobs.get(test).stream()
- .flatMap(status -> RunList.from(status)
- .on(versions)
- .matching(run -> run.id().type().zone().equals(type.zone()))
- .matching(Run::hasSucceeded)
- .asList().stream()
- .map(run -> run.end().get()))
- .min(naturalOrder()))
- .map(testedAt -> testedAt.map(Readiness::new).orElse(Readiness.unverified))
- .reduce(Readiness.empty, DeploymentStatus::max);
- }
-
- private Map<JobId, List<Job>> productionJobs(InstanceName instance, Change change, boolean assumeUpgradesSucceed) {
- Map<JobId, List<Job>> jobs = new LinkedHashMap<>();
- for (Entry<JobId, StepStatus> entry : reversed(List.copyOf(jobSteps.entrySet()))) {
- JobId job = entry.getKey();
- StepStatus step = entry.getValue();
- if ( ! job.application().instance().equals(instance) || ! job.type().isProduction())
- continue;
-
- // Signal strict completion criterion by depending on job itself.
- if (step.completedAt(change, Optional.of(job)).isPresent())
- continue;
-
- // When computing eager test jobs for outstanding changes, assume current change completes successfully.
- Optional<Deployment> deployment = deploymentFor(job);
- Optional<Version> existingPlatform = deployment.map(Deployment::version);
- Optional<RevisionId> existingRevision = deployment.map(Deployment::revision);
- boolean deployingCompatibilityChange = areIncompatible(existingPlatform, change.revision(), job)
- || areIncompatible(change.platform(), existingRevision, job);
- if (assumeUpgradesSucceed) {
- if (deployingCompatibilityChange) // No eager tests for this.
- continue;
-
- Change currentChange = application.require(instance).change();
- Versions target = Versions.from(currentChange, application, deployment, fallbackPlatform(currentChange, job));
- existingPlatform = Optional.of(target.targetPlatform());
- existingRevision = Optional.of(target.targetRevision());
- }
- List<Job> toRun = new ArrayList<>();
- List<Change> changes = deployingCompatibilityChange
- || allJobs.get(job).flatMap(status -> status.lastCompleted()).isEmpty()
- ? List.of(change)
- : changes(job, step, change);
- for (Change partial : changes) {
- Versions versions = Versions.from(partial, application, existingPlatform, existingRevision, fallbackPlatform(partial, job));
- Readiness readiness = step.readiness(partial, Optional.of(job));
- // This job is blocked if it is already running ...
- readiness = jobs().get(job).get().isRunning() && readiness.okAt(now) ? readiness.running() : readiness;
- // ... or if it is a deployment, and a test job for the current state is not yet complete,
- // which is the case when the next versions to run that test with is not the same as we want to deploy here.
- List<Job> tests = job.type().isTest() ? null : jobs.get(new JobId(job.application(), JobType.productionTestOf(job.type().zone())));
- readiness = tests != null && ! versions.targetsMatch(tests.get(0).versions) && readiness.okAt(now) ? readiness.blocked() : readiness;
- toRun.add(new Job(job.type(), versions, readiness, partial, null));
- // Assume first partial change is applied before the second.
- existingPlatform = Optional.of(versions.targetPlatform());
- existingRevision = Optional.of(versions.targetRevision());
- }
- jobs.put(job, toRun);
- }
- Map<JobId, List<Job>> jobsInOrder = new LinkedHashMap<>();
- for (Entry<JobId, List<Job>> entry : reversed(List.copyOf(jobs.entrySet())))
- jobsInOrder.put(entry.getKey(), entry.getValue());
- return jobsInOrder;
- }
-
- private boolean areIncompatible(Optional<Version> platform, Optional<RevisionId> revision, JobId job) {
- Optional<Version> compileVersion = revision.map(application.revisions()::get)
- .flatMap(ApplicationVersion::compileVersion);
- return platform.isPresent()
- && compileVersion.isPresent()
- && versionCompatibility.apply(job.application().instance()).refuse(platform.get(), compileVersion.get());
- }
-
- /** Changes to deploy with the given job, possibly split in two steps. */
- private List<Change> changes(JobId job, StepStatus step, Change change) {
- if ( change.platform().isEmpty() || change.revision().isEmpty()
- || change.isPlatformPinned() || change.isRevisionPinned())
- return List.of(change);
-
- if ( step.completedAt(change.withoutApplication(), Optional.of(job)).isPresent()
- || step.completedAt(change.withoutPlatform(), Optional.of(job)).isPresent())
- return List.of(change);
-
- // For a dual change, where both targets remain, we determine what to run by looking at when the two parts became ready:
- // for deployments, we look at dependencies; for production tests, this may be overridden by what is already deployed.
- JobId deployment = new JobId(job.application(), JobType.deploymentTo(job.type().zone()));
- UpgradeRollout rollout = application.deploymentSpec().requireInstance(job.application().instance()).upgradeRollout();
- if (job.type().isTest()) {
- Optional<Instant> platformDeployedAt = jobSteps.get(deployment).completedAt(change.withoutApplication(), Optional.of(deployment));
- Optional<Instant> revisionDeployedAt = jobSteps.get(deployment).completedAt(change.withoutPlatform(), Optional.of(deployment));
-
- // If only the revision has deployed, then we expect to test that first.
- if (platformDeployedAt.isEmpty() && revisionDeployedAt.isPresent()) return List.of(change.withoutPlatform(), change);
-
- // If only the upgrade has deployed, then we expect to test that first, with one exception:
- // The revision has caught up to the upgrade at the deployment job; and either
- // the upgrade is failing between deployment and here, or
- // the specified rollout is leading or simultaneous; and
- // the revision is now blocked by waiting for the production test to verify the upgrade.
- // In this case we must abandon the production test on the pure upgrade, so the revision can be deployed.
- if (platformDeployedAt.isPresent() && revisionDeployedAt.isEmpty()) {
- if (jobSteps.get(deployment).readiness(change, Optional.of(deployment)).okAt(now)) {
- return switch (rollout) {
- // If separate rollout, this test should keep blocking the revision, unless there are failures.
- case separate -> hasFailures(jobSteps.get(deployment), jobSteps.get(job)) ? List.of(change) : List.of(change.withoutApplication(), change);
- // If leading rollout, this test should now expect the two changes to fuse and roll together.
- case leading -> List.of(change);
- // If simultaneous rollout, this test should now expect the revision to run ahead.
- case simultaneous -> List.of(change.withoutPlatform(), change);
- };
- }
- return List.of(change.withoutApplication(), change);
- }
- // If neither is deployed, then neither is ready, and we assume the same order of changes as for the deployment job.
- if (platformDeployedAt.isEmpty())
- return changes(deployment, jobSteps.get(deployment), change);
-
- // If both are deployed, then we need to follow normal logic for what is ready.
- }
-
- Optional<Instant> platformReadyAt = step.dependenciesCompletedAt(change.withoutApplication(), Optional.of(job));
- Optional<Instant> revisionReadyAt = step.dependenciesCompletedAt(change.withoutPlatform(), Optional.of(job));
-
- boolean failingUpgradeOnlyTests = ! jobs().type(systemTest(job.type()), stagingTest(job.type()))
- .failingHardOn(Versions.from(change.withoutApplication(), application, deploymentFor(job), () -> systemVersion))
- .isEmpty();
-
- // If neither change is ready, we guess based on the specified rollout.
- if (platformReadyAt.isEmpty() && revisionReadyAt.isEmpty()) {
- return switch (rollout) {
- case separate -> ! failingUpgradeOnlyTests
- ? List.of(change.withoutApplication(), change) // Platform should stay ahead ...
- : List.of(change); // ... unless upgrade-only is failing tests.
- case leading -> List.of(change); // They should eventually join.
- case simultaneous -> List.of(change.withoutPlatform(), change); // Revision should get ahead.
- };
- }
-
- // If only the revision is ready, we run that first.
- if (platformReadyAt.isEmpty()) return List.of(change.withoutPlatform(), change);
-
- // If only the platform is ready, we run that first.
- if (revisionReadyAt.isEmpty()) return List.of(change.withoutApplication(), change);
-
- // Both changes are ready for this step, and we look to the specified rollout to decide.
- boolean platformReadyFirst = platformReadyAt.get().isBefore(revisionReadyAt.get());
- boolean revisionReadyFirst = revisionReadyAt.get().isBefore(platformReadyAt.get());
- return switch (rollout) {
- case separate -> // Let whichever change rolled out first, keep rolling first, unless upgrade alone is failing.
- (platformReadyFirst || platformReadyAt.get().equals(Instant.EPOCH)) // Assume platform was first if no jobs have run yet.
- ? step.job().flatMap(jobs()::get).flatMap(JobStatus::firstFailing).isPresent() || failingUpgradeOnlyTests
- ? List.of(change) // Platform was first, but is failing.
- : List.of(change.withoutApplication(), change) // Platform was first, and is OK.
- : revisionReadyFirst
- ? List.of(change.withoutPlatform(), change) // Revision was first.
- : List.of(change); // Both ready at the same time, probably due to earlier failure.
- case leading -> // When one change catches up, they fuse and continue together.
- List.of(change);
- case simultaneous -> // Revisions are allowed to run ahead, but the job where it caught up should have both changes.
- platformReadyFirst ? List.of(change) : List.of(change.withoutPlatform(), change);
- };
- }
-
- /** The test jobs that need to run prior to the given production deployment jobs. */
- public Map<JobId, List<Job>> testJobs(Map<JobId, List<Job>> jobs) {
- Map<JobId, List<Job>> testJobs = new LinkedHashMap<>();
- jobs.forEach((job, versionsList) -> {
- if (job.type().isProduction() && job.type().isDeployment()) {
- for (JobType testType : List.of(systemTest(job.type()), stagingTest(job.type()))) {
- prerequisiteTests(job, testType).forEach(testJob -> {
- for (Job productionJob : versionsList)
- if (allJobs.successOn(testType, productionJob.versions())
- .instance(testJob.application().instance())
- .asList().isEmpty()) {
- Readiness readiness = jobSteps().get(testJob).readiness(productionJob.change, Optional.of(job));
- testJobs.merge(testJob, List.of(new Job(testJob.type(),
- productionJob.versions(),
- readiness.okAt(now) && jobs().get(testJob).get().isRunning() ? readiness.running() : readiness,
- productionJob.change,
- job)),
- DeploymentStatus::union);
-
- }
- });
- }
- }
- });
- return Collections.unmodifiableMap(testJobs);
- }
-
- private CloudName findCloud(JobType job) {
- return zones.zones().all().get(job.zone()).map(ZoneApi::getCloudName).orElse(zones.systemZone().getCloudName());
- }
-
- private JobId firstDeclaredOrElseImplicitTest(JobType testJob) {
- return application.deploymentSpec().instanceNames().stream()
- .map(name -> new JobId(application.id().instance(name), testJob))
- .filter(jobSteps::containsKey)
- .min(comparing(id -> ! jobSteps.get(id).isDeclared())).orElseThrow();
- }
-
- /** JobId of any declared test of the given type, for the given instance. */
- private Optional<JobId> declaredTest(ApplicationId instanceId, JobType testJob) {
- JobId jobId = new JobId(instanceId, testJob);
- return jobSteps.containsKey(jobId) && jobSteps.get(jobId).isDeclared() ? Optional.of(jobId) : Optional.empty();
- }
-
- /** A DAG of the dependencies between the primitive steps in the spec, with iteration order equal to declaration order. */
- private Map<JobId, StepStatus> jobDependencies(DeploymentSpec spec, List<StepStatus> allSteps, Function<JobId, JobStatus> jobs) {
- if (DeploymentSpec.empty.equals(spec))
- return Map.of();
-
- Map<JobId, StepStatus> dependencies = new LinkedHashMap<>();
- List<StepStatus> previous = List.of();
- for (DeploymentSpec.Step step : spec.steps())
- previous = fillStep(dependencies, allSteps, step, previous, null, jobs,
- instanceWithImplicitTest(test, spec),
- instanceWithImplicitTest(staging, spec));
-
- return Collections.unmodifiableMap(dependencies);
- }
-
- private static InstanceName instanceWithImplicitTest(Environment environment, DeploymentSpec spec) {
- InstanceName first = null;
- for (DeploymentInstanceSpec step : spec.instances()) {
- if (step.concerns(environment)) return null;
- first = first != null ? first : step.name();
- }
- return first;
- }
-
- /**
- * Returns set of declared tests directly reachable from the given production job, or the first declared (or implicit) test.
- * A test in instance {@code I} is directly reachable from a job in instance {@code K} if a chain of instances {@code I, J, ..., K}
- * exists, such that only {@code I} has a declared test of the particular type.
- * These are the declared tests that should be OK before we proceed with the corresponding production deployment.
- * If no such tests exist, the first declared test, or a test in the first declared instance, is used instead.
- */
- private List<JobId> prerequisiteTests(JobId prodJob, JobType testType) {
- List<JobId> tests = new ArrayList<>();
- Set<InstanceName> seen = new LinkedHashSet<>();
- Deque<InstanceName> pending = new ArrayDeque<>();
- pending.add(prodJob.application().instance());
- while ( ! pending.isEmpty()) {
- InstanceName instance = pending.poll();
- Optional<JobId> test = declaredTest(application().id().instance(instance), testType);
- if (test.isPresent()) tests.add(test.get());
- else instanceSteps().get(instance).dependencies().stream().map(StepStatus::instance).forEach(dependency -> {
- if (seen.add(dependency)) pending.add(dependency);
- });
- }
- if (tests.isEmpty()) tests.add(firstDeclaredOrElseImplicitTest(testType));
- return tests;
- }
-
- /** Adds the primitive steps contained in the given step, which depend on the given previous primitives, to the dependency graph. */
- private List<StepStatus> fillStep(Map<JobId, StepStatus> dependencies, List<StepStatus> allSteps, DeploymentSpec.Step step,
- List<StepStatus> previous, InstanceName instance, Function<JobId, JobStatus> jobs,
- InstanceName implicitSystemTest, InstanceName implicitStagingTest) {
- if (step.steps().isEmpty() && ! (step instanceof DeploymentInstanceSpec)) {
- if (instance == null)
- return previous; // Ignore test and staging outside all instances.
-
- if ( ! step.delay().isZero()) {
- StepStatus stepStatus = new DelayStatus((DeploymentSpec.Delay) step, previous, instance);
- allSteps.add(stepStatus);
- return List.of(stepStatus);
- }
-
- JobType jobType;
- JobId jobId;
- StepStatus stepStatus;
- if (step.concerns(test) || step.concerns(staging)) {
- jobType = step.concerns(test) ? systemTest(null) : stagingTest(null);
- jobId = new JobId(application.id().instance(instance), jobType);
- stepStatus = JobStepStatus.ofTestDeployment((DeclaredZone) step, List.of(), this, jobs.apply(jobId), true);
- previous = new ArrayList<>(previous);
- previous.add(stepStatus);
- }
- else if (step.isTest()) {
- jobType = JobType.test(((DeclaredTest) step).region());
- jobId = new JobId(application.id().instance(instance), jobType);
- stepStatus = JobStepStatus.ofProductionTest((DeclaredTest) step, previous, this, jobs.apply(jobId));
- previous = List.of(stepStatus);
- }
- else if (step.concerns(prod)) {
- jobType = JobType.prod(((DeclaredZone) step).region().get());
- jobId = new JobId(application.id().instance(instance), jobType);
- stepStatus = JobStepStatus.ofProductionDeployment((DeclaredZone) step, previous, this, jobs.apply(jobId));
- previous = List.of(stepStatus);
- }
- else return previous; // Empty container steps end up here, and are simply ignored.
- allSteps.add(stepStatus);
- dependencies.put(jobId, stepStatus);
- return previous;
- }
-
- if (step instanceof DeploymentInstanceSpec) {
- DeploymentInstanceSpec spec = ((DeploymentInstanceSpec) step);
- StepStatus instanceStatus = new InstanceStatus(spec, previous, now, application.require(spec.name()), this);
- instance = spec.name();
- allSteps.add(instanceStatus);
- previous = List.of(instanceStatus);
- if (instance.equals(implicitSystemTest)) {
- JobId job = new JobId(application.id().instance(instance), systemTest(null));
- JobStepStatus testStatus = JobStepStatus.ofTestDeployment(new DeclaredZone(test), List.of(),
- this, jobs.apply(job), false);
- dependencies.put(job, testStatus);
- allSteps.add(testStatus);
- }
- if (instance.equals(implicitStagingTest)) {
- JobId job = new JobId(application.id().instance(instance), stagingTest(null));
- JobStepStatus testStatus = JobStepStatus.ofTestDeployment(new DeclaredZone(staging), List.of(),
- this, jobs.apply(job), false);
- dependencies.put(job, testStatus);
- allSteps.add(testStatus);
- }
- }
-
- if (step.isOrdered()) {
- for (DeploymentSpec.Step nested : step.steps())
- previous = fillStep(dependencies, allSteps, nested, previous, instance, jobs, implicitSystemTest, implicitStagingTest);
-
- return previous;
- }
-
- List<StepStatus> parallel = new ArrayList<>();
- for (DeploymentSpec.Step nested : step.steps())
- parallel.addAll(fillStep(dependencies, allSteps, nested, previous, instance, jobs, implicitSystemTest, implicitStagingTest));
-
- return List.copyOf(parallel);
- }
-
-
- public enum StepType {
-
- /** An instance — completion marks a change as ready for the jobs contained in it. */
- instance,
-
- /** A timed delay. */
- delay,
-
- /** A system, staging or production test. */
- test,
-
- /** A production deployment. */
- deployment,
- }
-
- /**
- * Used to represent all steps — explicit and implicit — that may run in order to complete deployment of a change.
- *
- * Each node contains a step describing the node,
- * a list of steps which need to be complete before the step may start,
- * a list of jobs from which completion of the step is computed, and
- * optionally, an instance name used to identify a job type for the step,
- *
- * The completion criterion for each type of step is implemented in subclasses of this.
- */
- public static abstract class StepStatus {
-
- private final StepType type;
- private final DeploymentSpec.Step step;
- private final List<StepStatus> dependencies; // All direct dependencies of this step.
- private final InstanceName instance;
-
- private StepStatus(StepType type, DeploymentSpec.Step step, List<StepStatus> dependencies, InstanceName instance) {
- this.type = requireNonNull(type);
- this.step = requireNonNull(step);
- this.dependencies = List.copyOf(dependencies);
- this.instance = instance;
- }
-
- /** The type of step this is. */
- public final StepType type() { return type; }
-
- /** The step defining this. */
- public final DeploymentSpec.Step step() { return step; }
-
- /** The list of steps that need to be complete before this may start. */
- public final List<StepStatus> dependencies() { return dependencies; }
-
- /** The instance of this. */
- public final InstanceName instance() { return instance; }
-
- /** The id of the job this corresponds to, if any. */
- public Optional<JobId> job() { return Optional.empty(); }
-
- /** The time at which this is, or was, complete on the given change and / or versions. */
- public Optional<Instant> completedAt(Change change) { return completedAt(change, Optional.empty()); }
-
- /** The time at which this is, or was, complete on the given change and / or versions. */
- abstract Optional<Instant> completedAt(Change change, Optional<JobId> dependent);
-
- /** The time at which this step is ready to run the specified change and / or versions. */
- public Readiness readiness(Change change) { return readiness(change, Optional.empty()); }
-
- /** The time at which this step is ready to run the specified change and / or versions. */
- Readiness readiness(Change change, Optional<JobId> dependent) {
- return dependenciesCompletedAt(change, dependent)
- .map(Readiness::new)
- .map(ready -> Stream.of(blockedUntil(change),
- pausedUntil(),
- coolingDownUntil(change, dependent))
- .reduce(ready, maxBy(naturalOrder())))
- .orElse(Readiness.notReady);
- }
-
- /** The time at which all dependencies completed on the given change and / or versions. */
- Optional<Instant> dependenciesCompletedAt(Change change, Optional<JobId> dependent) {
- Instant latest = Instant.EPOCH;
- for (StepStatus step : dependencies) {
- Optional<Instant> completedAt = step.completedAt(change, dependent);
- if (completedAt.isEmpty()) return Optional.empty();
- latest = latest.isBefore(completedAt.get()) ? completedAt.get() : latest;
- }
- return Optional.of(latest);
- }
-
- /** The time until which this step is blocked by a change blocker. */
- public Readiness blockedUntil(Change change) { return Readiness.empty; }
-
- /** The time until which this step is paused by user intervention. */
- public Readiness pausedUntil() { return Readiness.empty; }
-
- /** The time until which this step is cooling down, due to consecutive failures. */
- public Readiness coolingDownUntil(Change change, Optional<JobId> dependent) { return Readiness.empty; }
-
- /** Whether this step is declared in the deployment spec, or is an implicit step. */
- public boolean isDeclared() { return true; }
-
- }
-
-
- private static class DelayStatus extends StepStatus {
-
- private DelayStatus(DeploymentSpec.Delay step, List<StepStatus> dependencies, InstanceName instance) {
- super(StepType.delay, step, dependencies, instance);
- }
-
- @Override
- Optional<Instant> completedAt(Change change, Optional<JobId> dependent) {
- return Optional.ofNullable(readiness(change, dependent).at())
- .map(completion -> completion.plus(step().delay()));
- }
-
- }
-
-
- private static class InstanceStatus extends StepStatus {
-
- private final DeploymentInstanceSpec spec;
- private final Instant now;
- private final Instance instance;
- private final DeploymentStatus status;
-
- private InstanceStatus(DeploymentInstanceSpec spec, List<StepStatus> dependencies, Instant now,
- Instance instance, DeploymentStatus status) {
- super(StepType.instance, spec, dependencies, spec.name());
- this.spec = spec;
- this.now = now;
- this.instance = instance;
- this.status = status;
- }
-
- /** The time at which this step is ready to run the specified change and / or versions. */
- @Override
- public Readiness readiness(Change change) {
- return status.jobSteps.keySet().stream()
- .filter(job -> job.type().isProduction() && job.application().instance().equals(instance.name()))
- .map(job -> super.readiness(change, Optional.of(job)))
- .reduce((a, b) -> ! a.ok() ? a : ! b.ok() ? b : min(a, b))
- .orElseGet(() -> super.readiness(change, Optional.empty()));
- }
-
- /**
- * Time of completion of its dependencies, if all parts of the given change are contained in the change
- * for this instance, or if no more jobs should run for this instance for the given change.
- */
- @Override
- Optional<Instant> completedAt(Change change, Optional<JobId> dependent) {
- return ( (change.platform().isEmpty() || change.platform().equals(instance.change().platform()))
- && (change.revision().isEmpty() || change.revision().equals(instance.change().revision()))
- || step().steps().stream().noneMatch(step -> step.concerns(prod)))
- ? dependenciesCompletedAt(change, dependent).or(() -> Optional.of(Instant.EPOCH).filter(__ -> change.hasTargets()))
- : Optional.empty();
- }
-
- @Override
- public Readiness blockedUntil(Change change) {
- for (Instant current = now; now.plus(Duration.ofDays(7)).isAfter(current); ) {
- boolean blocked = false;
- for (DeploymentSpec.ChangeBlocker blocker : spec.changeBlocker()) {
- while ( blocker.window().includes(current)
- && now.plus(Duration.ofDays(7)).isAfter(current)
- && ( change.platform().isPresent() && blocker.blocksVersions()
- || change.revision().isPresent() && blocker.blocksRevisions())) {
- blocked = true;
- current = current.plus(Duration.ofHours(1)).truncatedTo(ChronoUnit.HOURS);
- }
- }
- if ( ! blocked)
- return current == now ? Readiness.empty : new Readiness(current, DelayCause.changeBlocked);
- }
- return new Readiness(now.plusSeconds(1 << 30), DelayCause.changeBlocked); // Some time in the future that doesn't look like anything you'd expect.
- }
-
- }
-
-
- private static abstract class JobStepStatus extends StepStatus {
-
- private final JobStatus job;
- private final DeploymentStatus status;
-
- private JobStepStatus(StepType type, DeploymentSpec.Step step, List<StepStatus> dependencies, JobStatus job,
- DeploymentStatus status) {
- super(type, step, dependencies, job.id().application().instance());
- this.job = requireNonNull(job);
- this.status = requireNonNull(status);
- }
-
- @Override
- public Optional<JobId> job() { return Optional.of(job.id()); }
-
- @Override
- public Readiness pausedUntil() {
- return status.application().require(job.id().application().instance()).jobPause(job.id().type())
- .map(pause -> new Readiness(pause, DelayCause.paused))
- .orElse(Readiness.empty);
- }
-
- @Override
- public Readiness coolingDownUntil(Change change, Optional<JobId> dependent) {
- if (job.lastTriggered().isEmpty()) return Readiness.empty;
- if (job.lastCompleted().isEmpty()) return Readiness.empty;
- if (job.firstFailing().isEmpty() || ! job.firstFailing().get().hasEnded()) return Readiness.empty;
- Versions lastVersions = job.lastCompleted().get().versions();
- Versions toRun = Versions.from(change, status.application, dependent.flatMap(status::deploymentFor), status.fallbackPlatform(change, job.id()));
- if ( ! toRun.targetsMatch(lastVersions)) return Readiness.empty;
- if ( job.id().type().environment().isTest()
- && ! dependent.map(JobId::type).map(status::findCloud).map(List.of(CloudName.AWS, CloudName.GCP)::contains).orElse(true)
- && job.isNodeAllocationFailure()) return Readiness.empty;
-
- if (job.lastStatus().get() == invalidApplication) return new Readiness(status.now.plus(Duration.ofSeconds(1 << 30)), DelayCause.invalidPackage);
- if (job.lastStatus().get() == cancelled) return new Readiness(status.now.plus(Duration.ofSeconds(1 << 30)), DelayCause.coolingDown);
- Instant firstFailing = job.firstFailing().get().end().get();
- Instant lastCompleted = job.lastCompleted().get().end().get();
-
- Duration penalty = firstFailing.equals(lastCompleted) ? Duration.ZERO
- : Duration.ofMinutes(10)
- .plus(Duration.between(firstFailing, lastCompleted)
- .dividedBy(2));
- return lastCompleted.plus(penalty).isAfter(status.now) ? new Readiness(lastCompleted.plus(penalty), DelayCause.coolingDown)
- : Readiness.empty;
- }
-
- private static JobStepStatus ofProductionDeployment(DeclaredZone step, List<StepStatus> dependencies,
- DeploymentStatus status, JobStatus job) {
- ZoneId zone = ZoneId.from(step.environment(), step.region().get());
- Optional<Deployment> existingDeployment = Optional.ofNullable(status.application().require(job.id().application().instance())
- .deployments().get(zone));
-
- return new JobStepStatus(StepType.deployment, step, dependencies, job, status) {
-
- @Override
- public Readiness readiness(Change change, Optional<JobId> dependent) {
- Readiness readyAt = super.readiness(change, dependent);
- Readiness testedAt = status.verifiedAt(job.id(), Versions.from(change, status.application, existingDeployment, status.fallbackPlatform(change, job.id())));
- return max(readyAt, testedAt);
- }
-
- /** Complete if deployment is on pinned version, and last successful deployment, or if given versions is strictly a downgrade, and this isn't forced by a pin. */
- @Override
- Optional<Instant> completedAt(Change change, Optional<JobId> dependent) {
- if ( change.isPlatformPinned()
- && change.platform().isPresent()
- && ! existingDeployment.map(Deployment::version).equals(change.platform()))
- return Optional.empty();
-
- if ( change.revision().isPresent()
- && change.isRevisionPinned()
- && ! existingDeployment.map(Deployment::revision).equals(change.revision()))
- return Optional.empty();
-
- Change fullChange = status.application().require(job.id().application().instance()).change();
- if (existingDeployment.map(deployment -> ! (change.upgrades(deployment.version()) || change.upgrades(deployment.revision()))
- && (fullChange.downgrades(deployment.version()) || fullChange.downgrades(deployment.revision())))
- .orElse(false))
- return job.lastCompleted().flatMap(Run::end);
-
- Optional<Instant> end = Optional.empty();
- for (Run run : job.runs().descendingMap().values()) {
- if (run.versions().targetsMatch(change)) {
- if (run.hasSucceeded()) end = run.end();
- }
- else if (dependent.equals(job())) // If strict completion, consider only last time this change was deployed.
- break;
- }
- return end;
- }
- };
- }
-
- private static JobStepStatus ofProductionTest(DeclaredTest step, List<StepStatus> dependencies,
- DeploymentStatus status, JobStatus job) {
- JobId prodId = new JobId(job.id().application(), JobType.deploymentTo(job.id().type().zone()));
- return new JobStepStatus(StepType.test, step, dependencies, job, status) {
- @Override
- Readiness readiness(Change change, Optional<JobId> dependent) {
- Readiness readyAt = super.readiness(change, dependent);
- Readiness deployedAt = status.jobSteps().get(prodId).completedAt(change, Optional.of(prodId))
- .map(Readiness::new).orElse(Readiness.notReady);
- return max(readyAt, deployedAt);
- }
-
- @Override
- Optional<Instant> completedAt(Change change, Optional<JobId> dependent) {
- Optional<Instant> deployedAt = status.jobSteps().get(prodId).completedAt(change, Optional.of(prodId));
- Versions target = Versions.from(change, status.application(), status.deploymentFor(job.id()), status.fallbackPlatform(change, job.id()));
- Change applied = Change.empty();
- if (change.platform().isPresent())
- applied = applied.with(target.targetPlatform());
- if (change.revision().isPresent())
- applied = applied.with(target.targetRevision());
- Change relevant = applied;
-
- return (dependent.equals(job()) ? job.lastTriggered().filter(run -> deployedAt.map(at -> ! run.start().isBefore(at)).orElse(false)).stream()
- : job.runs().values().stream())
- .filter(Run::hasSucceeded)
- .filter(run -> run.versions().targetsMatch(relevant))
- .flatMap(run -> run.end().stream()).findFirst();
- }
- };
- }
-
- private static JobStepStatus ofTestDeployment(DeclaredZone step, List<StepStatus> dependencies,
- DeploymentStatus status, JobStatus job, boolean declared) {
- return new JobStepStatus(StepType.test, step, dependencies, job, status) {
- @Override
- Optional<Instant> completedAt(Change change, Optional<JobId> dependent) {
- Optional<ZoneId> requiredTestZone = dependent.map(dep -> job.id().type().isSystemTest() ? status.systemTest(dep.type()).zone()
- : status.stagingTest(dep.type()).zone());
- return RunList.from(job)
- .matching(run -> dependent.flatMap(status::deploymentFor)
- .map(deployment -> run.versions().targetsMatch(Versions.from(change,
- status.application,
- Optional.of(deployment),
- status.fallbackPlatform(change, dependent.get()))))
- .orElseGet(() -> (change.platform().isEmpty() || change.platform().get().equals(run.versions().targetPlatform()))
- && (change.revision().isEmpty() || change.revision().get().equals(run.versions().targetRevision()))))
- .matching(Run::hasSucceeded)
- .matching(run -> requiredTestZone.isEmpty() || requiredTestZone.get().equals(run.id().type().zone()))
- .asList().stream()
- .map(run -> run.end().get())
- .max(naturalOrder());
- }
-
- @Override
- public boolean isDeclared() { return declared; }
- };
- }
-
- }
-
- public static class Job {
-
- private final JobType type;
- private final Versions versions;
- private final Readiness readiness;
- private final Change change;
- private final JobId dependent;
-
- public Job(JobType type, Versions versions, Readiness readiness, Change change, JobId dependent) {
- this.type = type;
- this.versions = type.isSystemTest() ? versions.withoutSources() : versions;
- this.readiness = readiness;
- this.change = change;
- this.dependent = dependent;
- }
-
- public JobType type() {
- return type;
- }
-
- public Versions versions() {
- return versions;
- }
-
- public Readiness readiness() {
- return readiness;
- }
-
- public Reason reason() {
- return new Reason(Optional.empty(), Optional.ofNullable(dependent), Optional.ofNullable(change));
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Job job = (Job) o;
- return type.zone().equals(job.type.zone()) && versions.equals(job.versions) && readiness.equals(job.readiness) && change.equals(job.change);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(type.zone(), versions, readiness, change);
- }
-
- @Override
- public String toString() {
- return change + " with versions " + versions + ", " + readiness;
- }
-
- }
-
- public enum DelayCause { none, unverified, notReady, blocked, running, coolingDown, invalidPackage, changeBlocked, paused }
- public record Readiness(Instant at, DelayCause cause) implements Comparable<Readiness> {
- public static final Readiness unverified = new Readiness(null, DelayCause.unverified);
- public static final Readiness notReady = new Readiness(null, DelayCause.notReady);
- public static final Readiness empty = new Readiness(Instant.EPOCH, DelayCause.none);
- public Readiness(Instant at) { this(at, DelayCause.none); }
- public Readiness blocked() { return new Readiness(at, DelayCause.blocked); }
- public Readiness running() { return new Readiness(at, DelayCause.running); }
- public boolean ok() { return at != null; }
- public boolean okAt(Instant at) { return ok() && cause != DelayCause.running && cause != DelayCause.blocked && ! at.isBefore(this.at); }
- @Override public int compareTo(Readiness o) {
- return at == null ? o.at == null ? 0 : 1
- : o.at == null ? -1 : at.compareTo(o.at);
- }
- @Override public String toString() {
- return ok() ? "ready at " + at + switch (cause) {
- case none -> "";
- case coolingDown -> ": cooling down after repeated failures";
- case blocked -> ": waiting for verification test to complete";
- case running -> ": waiting for current run to complete";
- case invalidPackage -> ": invalid application package, must resubmit";
- case changeBlocked -> ": deployment configuration blocks changes";
- case paused -> ": manually paused";
- default -> throw new IllegalStateException(cause + " should not have an instant at which it is ready");
- }
- : "not ready" + switch (cause) {
- case unverified -> ": waiting for verification test to complete";
- case notReady -> ": waiting for dependencies to complete";
- default -> throw new IllegalStateException(cause + " should have an instant at which it is ready");
- };
- }
- }
-
- static <T extends Comparable<T>> T min(T a, T b) {
- return a.compareTo(b) > 0 ? b : a;
- }
-
- static <T extends Comparable<T>> T max(T a, T b) {
- return a.compareTo(b) < 0 ? b : a;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatusList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatusList.java
deleted file mode 100644
index 16bd5bd9bb2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentStatusList.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.collections.AbstractFilteringList;
-import com.yahoo.component.Version;
-import com.yahoo.vespa.hosted.controller.application.Change;
-
-import java.time.Instant;
-import java.util.Collection;
-
-/**
- * List for filtering deployment status of applications, for inspection and decision making.
- *
- * @author jonmv
- */
-public class DeploymentStatusList extends AbstractFilteringList<DeploymentStatus, DeploymentStatusList> {
-
- private DeploymentStatusList(Collection<? extends DeploymentStatus> items, boolean negate) {
- super(items, negate, DeploymentStatusList::new);
- }
-
- public static DeploymentStatusList from(Collection<? extends DeploymentStatus> status) {
- return new DeploymentStatusList(status, false);
- }
-
- /** Returns the subset of applications which have changes left to deploy; blocked, or deploying */
- public DeploymentStatusList withChanges() {
- return matching(status -> status.application().productionInstances().values().stream()
- .anyMatch(instance -> instance.change().hasTargets() || status.outstandingChange(instance.name()).hasTargets()));
- }
-
- /** Returns the subset of applications which have been failing an upgrade to the given version since the given instant */
- public DeploymentStatusList failingUpgradeToVersionSince(Version version, Instant threshold) {
- return matching(status -> status.instanceJobs().values().stream()
- .anyMatch(jobs -> failingUpgradeToVersionSince(jobs, version, threshold)));
- }
-
- /** Returns the subset of applications which have been failing an application change since the given instant */
- public DeploymentStatusList failingApplicationChangeSince(Instant threshold) {
- return matching(status -> status.instanceJobs().entrySet().stream()
- .anyMatch(jobs -> failingApplicationChangeSince(jobs.getValue(),
- status.application().require(jobs.getKey().instance()).change(),
- threshold)));
- }
-
- private static boolean failingUpgradeToVersionSince(JobList jobs, Version version, Instant threshold) {
- return ! jobs.not().failingApplicationChange()
- .firstFailing().endedNoLaterThan(threshold)
- .lastCompleted().on(version)
- .isEmpty();
- }
-
- private static boolean failingApplicationChangeSince(JobList jobs, Change change, Instant threshold) {
- return change.revision().map(revision -> ! jobs.failingWithBrokenRevisionSince(revision, threshold).isEmpty()).orElse(false);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
deleted file mode 100644
index 834efa81d26..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTrigger.java
+++ /dev/null
@@ -1,505 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus.DelayCause;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus.Readiness;
-import com.yahoo.vespa.hosted.controller.deployment.Run.Reason;
-
-import java.math.BigDecimal;
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalLong;
-import java.util.function.Predicate;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-import static java.util.Comparator.comparing;
-import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.toMap;
-
-/**
- * Responsible for scheduling deployment jobs in a build system and keeping
- * {@link Instance#change()} in sync with what is scheduled.
- *
- * This class is multi-thread safe.
- *
- * @author bratseth
- * @author mpolden
- * @author jonmv
- */
-public class DeploymentTrigger {
-
- public static final Duration maxPause = Duration.ofDays(3);
- public static final Duration maxFailingRevisionTime = Duration.ofDays(5);
- private final static Logger log = Logger.getLogger(DeploymentTrigger.class.getName());
-
- private final Controller controller;
- private final Clock clock;
- private final JobController jobs;
-
- public DeploymentTrigger(Controller controller, Clock clock) {
- this.controller = Objects.requireNonNull(controller, "controller cannot be null");
- this.clock = Objects.requireNonNull(clock, "clock cannot be null");
- this.jobs = controller.jobController();
- }
-
- /**
- * Propagates the latest revision to ready instances.
- * Ready instances are those whose dependencies are complete, and which aren't blocked, and, additionally,
- * which aren't upgrading, or are already deploying an application change, or failing upgrade.
- */
- public void triggerNewRevision(TenantAndApplicationId id) {
- applications().lockApplicationIfPresent(id, application -> {
- DeploymentStatus status = jobs.deploymentStatus(application.get());
- for (InstanceName instanceName : application.get().deploymentSpec().instanceNames()) {
- Change outstanding = status.outstandingChange(instanceName);
- boolean deployOutstanding = outstanding.hasTargets()
- && status.instanceSteps().get(instanceName)
- .readiness(outstanding).okAt(clock.instant())
- && acceptNewRevision(status, instanceName, outstanding.revision().get());
- application = application.with(instanceName,
- instance -> withRemainingChange(instance,
- deployOutstanding ? outstanding.onTopOf(instance.change())
- : instance.change(),
- status,
- false));
- }
-
- // If app has been broken since it was first submitted, and not fixed for a long time, we stop managing it until a new submission comes in.
- if (applicationWasAlwaysBroken(status))
- application = application.withProjectId(OptionalLong.empty());
-
- applications().store(application);
- });
- }
-
- private boolean applicationWasAlwaysBroken(DeploymentStatus status) {
- // If application has a production deployment, we cannot forget it.
- if (status.application().instances().values().stream().anyMatch(instance -> ! instance.productionDeployments().isEmpty()))
- return false;
-
- // Then, we need a job that always failed, and failed on the last revision for at least 30 days.
- RevisionId last = status.application().revisions().last().get().id();
- Instant threshold = clock.instant().minus(Duration.ofDays(30));
- for (JobStatus job : status.jobs().asList())
- for (Run run : job.runs().descendingMap().values())
- if (run.hasEnded() && ! run.hasFailed() || ! run.versions().targetRevision().equals(last)) break;
- else if (run.start().isBefore(threshold)) return true;
-
- return false;
- }
-
- /**
- * Records information when a job completes (successfully or not). This information is used when deciding what to
- * trigger next.
- */
- public void notifyOfCompletion(ApplicationId id) {
- if (applications().getInstance(id).isEmpty()) {
- log.log(Level.WARNING, "Ignoring completion of job of unknown application '" + id + "'");
- return;
- }
-
- applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- if (application.get().deploymentSpec().instance(id.instance()).isPresent())
- applications().store(application.with(id.instance(),
- instance -> withRemainingChange(instance,
- instance.change(),
- jobs.deploymentStatus(application.get()),
- true)));
- });
- }
-
- /**
- * Finds and triggers jobs that can and should run but are currently not, and returns the number of triggered jobs.
- * Only one job per type is triggered each run for test jobs, since their environments have limited capacity.
- */
- public TriggerResult triggerReadyJobs() {
- List<Job> readyJobs = computeReadyJobs();
-
- var prodJobs = new ArrayList<Job>();
- var testJobs = new ArrayList<Job>();
- for (Job job : readyJobs)
- (job.jobType().isProduction() ? prodJobs : testJobs).add(job);
-
- // Flat list of prod jobs, grouped by application id, retaining the step order
- List<Job> sortedProdJobs = prodJobs.stream()
- .collect(groupingBy(Job::applicationId))
- .values().stream()
- .flatMap(List::stream)
- .toList();
-
- // Map of test jobs, a list for each job type. Jobs in each list are sorted by priority.
- Map<JobType, List<Job>> sortedTestJobsByType = testJobs.stream()
- .sorted(comparing(Job::isRetry)
- .thenComparing(Job::applicationUpgrade)
- .reversed()
- .thenComparing(Job::availableSince))
- .collect(groupingBy(Job::jobType));
-
- // Trigger all prod jobs
- long triggeredJobs = 0;
- long failedJobs = 0;
- for (Job job : sortedProdJobs) {
- if (trigger(job)) ++triggeredJobs;
- else ++failedJobs;
- }
-
- // Trigger max one test job per type
- for (Collection<Job> jobs: sortedTestJobsByType.values())
- for (Job job : jobs)
- if (trigger(job)) { ++triggeredJobs; break; }
- else ++failedJobs;
-
- return new TriggerResult(triggeredJobs, failedJobs);
- }
-
- public record TriggerResult(long triggered, long failed) { }
-
- /** Attempts to trigger the given job. */
- private boolean trigger(Job job) {
- try {
- log.log(Level.FINE, () -> "Triggering " + job);
- applications().lockApplicationOrThrow(TenantAndApplicationId.from(job.applicationId()), application -> {
- jobs.start(job.applicationId(), job.jobType, job.versions, false, job.reason);
- applications().store(application.with(job.applicationId().instance(), instance ->
- instance.withJobPause(job.jobType, OptionalLong.empty())));
- });
- return true;
- }
- catch (Exception e) {
- log.log(Level.WARNING, "Failed triggering " + job.jobType() + " for " + job.instanceId, e);
- return false;
- }
- }
-
- /** Force triggering of a job for given instance, with same versions as last run. */
- public JobId reTrigger(ApplicationId applicationId, JobType jobType, String reason) {
- Application application = applications().requireApplication(TenantAndApplicationId.from(applicationId));
- Instance instance = application.require(applicationId.instance());
- JobId job = new JobId(instance.id(), jobType);
- JobStatus jobStatus = jobs.jobStatus(new JobId(applicationId, jobType));
- Run last = jobStatus.lastTriggered()
- .orElseThrow(() -> new IllegalArgumentException(job + " has never been triggered"));
- trigger(deploymentJob(instance, last.versions(), last.id().type(), jobStatus.isNodeAllocationFailure(), clock.instant(),
- new Reason(Optional.ofNullable(reason), last.reason().dependent(), last.reason().change())));
- return job;
- }
-
- /** Force triggering of a job for given instance. */
- public List<JobId> forceTrigger(ApplicationId applicationId, JobType jobType, String reason, boolean requireTests,
- boolean upgradeRevision, boolean upgradePlatform) {
- Application application = applications().requireApplication(TenantAndApplicationId.from(applicationId));
- Instance instance = application.require(applicationId.instance());
- DeploymentStatus status = jobs.deploymentStatus(application);
- if (jobType.environment().isTest()) {
- CloudName cloud = status.firstDependentProductionJobsWithDeployment(applicationId.instance()).keySet().stream().findFirst()
- .orElse(controller.zoneRegistry().systemZone().getCloudName());
- jobType = jobType.isSystemTest() ? JobType.systemTest(controller.zoneRegistry(), cloud)
- : JobType.stagingTest(controller.zoneRegistry(), cloud);
- }
- JobId job = new JobId(instance.id(), jobType);
- if (job.type().environment().isManuallyDeployed())
- return forceTriggerManualJob(job, reason);
-
- Change change = instance.change();
- if ( ! upgradeRevision && change.revision().isPresent()) change = change.withoutApplication();
- if ( ! upgradePlatform && change.platform().isPresent()) change = change.withoutPlatform();
- Versions versions = Versions.from(change, application, status.deploymentFor(job), status.fallbackPlatform(change, job));
- DeploymentStatus.Job toTrigger = new DeploymentStatus.Job(job.type(), versions, new Readiness(controller.clock().instant()), instance.change(), null);
- Map<JobId, List<DeploymentStatus.Job>> testJobs = status.testJobs(Map.of(job, List.of(toTrigger)));
-
- Map<JobId, List<DeploymentStatus.Job>> jobs = testJobs.isEmpty() || ! requireTests
- ? Map.of(job, List.of(toTrigger))
- : testJobs.entrySet().stream()
- .filter(entry -> controller.jobController().last(entry.getKey()).map(Run::hasEnded).orElse(true))
- .collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- jobs.forEach((jobId, jobList) -> {
- trigger(deploymentJob(application.require(jobId.application().instance()),
- jobList.get(0).versions(),
- jobId.type(),
- status.jobs().get(jobId).get().isNodeAllocationFailure(),
- clock.instant(),
- new Reason(Optional.of(reason), jobList.get(0).reason().dependent(), jobList.get(0).reason().change())));
- });
- return List.copyOf(jobs.keySet());
- }
-
- private List<JobId> forceTriggerManualJob(JobId job, String reason) {
- Run last = jobs.last(job).orElseThrow(() -> new IllegalArgumentException(job + " has never been run"));
- Versions target = new Versions(controller.readSystemVersion(),
- last.versions().targetRevision(),
- Optional.of(last.versions().targetPlatform()),
- Optional.of(last.versions().targetRevision()));
- jobs.start(job.application(), job.type(), target, true, Reason.because(reason));
- return List.of(job);
- }
-
- /** Retrigger job. If the job is already running, it will be canceled, and retrigger enqueued. */
- public Optional<JobId> reTriggerOrAddToQueue(DeploymentId deployment, String reason) {
- JobType jobType = JobType.deploymentTo(deployment.zoneId());
- Optional<Run> existingRun = controller.jobController().active(deployment.applicationId()).stream()
- .filter(run -> run.id().type().equals(jobType))
- .findFirst();
-
- if (existingRun.isPresent()) {
- Run run = existingRun.get();
- try (Mutex lock = controller.curator().lockDeploymentRetriggerQueue()) {
- List<RetriggerEntry> retriggerEntries = controller.curator().readRetriggerEntries();
- List<RetriggerEntry> newList = new ArrayList<>(retriggerEntries);
- RetriggerEntry requiredEntry = new RetriggerEntry(new JobId(deployment.applicationId(), jobType), run.id().number() + 1);
- if (newList.stream().noneMatch(entry -> entry.jobId().equals(requiredEntry.jobId()) && entry.requiredRun() >= requiredEntry.requiredRun())) {
- newList.add(requiredEntry);
- }
- newList = newList.stream()
- .filter(entry -> !(entry.jobId().equals(requiredEntry.jobId()) && entry.requiredRun() < requiredEntry.requiredRun()))
- .toList();
- controller.curator().writeRetriggerEntries(newList);
- }
- controller.jobController().abort(run.id(), "force re-triggered", false);
- return Optional.empty();
- } else {
- return Optional.of(reTrigger(deployment.applicationId(), jobType, reason));
- }
- }
-
- /** Prevents jobs of the given type from starting, until the given time. */
- public void pauseJob(ApplicationId id, JobType jobType, Instant until) {
- if (until.isAfter(clock.instant().plus(maxPause)))
- throw new IllegalArgumentException("Pause only allowed for up to " + maxPause);
-
- applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application ->
- applications().store(application.with(id.instance(),
- instance -> instance.withJobPause(jobType, OptionalLong.of(until.toEpochMilli())))));
- }
-
- /** Resumes a previously paused job, letting it be triggered normally. */
- public void resumeJob(ApplicationId id, JobType jobType) {
- applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application ->
- applications().store(application.with(id.instance(),
- instance -> instance.withJobPause(jobType, OptionalLong.empty()))));
- }
-
- /** Overrides the given instance's platform and application changes with any contained in the given change. */
- public void forceChange(ApplicationId instanceId, Change change) {
- forceChange(instanceId, change, true);
- }
-
- /** Overrides the given instance's platform and application changes with any contained in the given change. */
- public void forceChange(ApplicationId instanceId, Change change, boolean allowOutdatedPlatform) {
- applications().lockApplicationOrThrow(TenantAndApplicationId.from(instanceId), application -> {
- applications().store(application.with(instanceId.instance(),
- instance -> withRemainingChange(instance,
- change.onTopOf(instance.change()),
- jobs.deploymentStatus(application.get()),
- allowOutdatedPlatform)));
- });
- }
-
- /** Cancels the indicated part of the given application's change. */
- public void cancelChange(ApplicationId instanceId, ChangesToCancel cancellation) {
- applications().lockApplicationOrThrow(TenantAndApplicationId.from(instanceId), application -> {
- Change change = switch (cancellation) {
- case ALL -> Change.empty();
- case PLATFORM -> application.get().require(instanceId.instance()).change().withoutPlatform();
- case APPLICATION -> application.get().require(instanceId.instance()).change().withoutApplication();
- case PIN -> application.get().require(instanceId.instance()).change().withoutPlatformPin();
- case PLATFORM_PIN -> application.get().require(instanceId.instance()).change().withoutPlatformPin();
- case APPLICATION_PIN -> application.get().require(instanceId.instance()).change().withoutRevisionPin();
- };
- applications().store(application.with(instanceId.instance(),
- instance -> withRemainingChange(instance,
- change,
- jobs.deploymentStatus(application.get()),
- true)));
- });
- }
-
- public enum ChangesToCancel { ALL, PLATFORM, APPLICATION, PIN, PLATFORM_PIN, APPLICATION_PIN }
-
- // ---------- Conveniences ----------
-
- private ApplicationController applications() {
- return controller.applications();
- }
-
- // ---------- Ready job computation ----------
-
- /** Returns the set of all jobs which have changes to propagate from the upstream steps. */
- private List<Job> computeReadyJobs() {
- return jobs.deploymentStatuses(ApplicationList.from(applications().readable())
- .withProjectId() // Need to keep this, as we have applications with deployment spec that shouldn't be orchestrated.
- .withJobs())
- .withChanges()
- .asList().stream()
- .filter(status -> ! hasExceededQuota(status.application().id().tenant()))
- .map(this::computeReadyJobs)
- .flatMap(Collection::stream)
- .toList();
- }
-
- /** Finds the next step to trigger for the given application, if any, and returns these as a list. */
- private List<Job> computeReadyJobs(DeploymentStatus status) {
- List<Job> jobs = new ArrayList<>();
- Map<JobId, List<DeploymentStatus.Job>> jobsToRun = status.jobsToRun();
- jobsToRun.forEach((jobId, jobsList) -> {
- abortIfOutdated(status, jobsToRun, jobId);
- DeploymentStatus.Job job = jobsList.get(0);
- if ( job.readiness().okAt(clock.instant())
- && ! controller.jobController().isDisabled(new JobId(jobId.application(), job.type()))
- && ! (jobId.type().isProduction() && isUnhealthyInAnotherZone(status.application(), jobId))) {
- jobs.add(deploymentJob(status.application().require(jobId.application().instance()),
- job.versions(),
- job.type(),
- status.instanceJobs(jobId.application().instance()).get(jobId.type()).isNodeAllocationFailure(),
- job.readiness().at(),
- job.reason()));
- }
- });
- return Collections.unmodifiableList(jobs);
- }
-
- private boolean hasExceededQuota(TenantName tenant) {
- return controller.serviceRegistry().billingController().getQuota(tenant).budget().equals(Optional.of(BigDecimal.ZERO));
- }
-
- /** Returns whether the application is healthy in all other production zones. */
- private boolean isUnhealthyInAnotherZone(Application application, JobId job) {
- for (Deployment deployment : application.require(job.application().instance()).productionDeployments().values()) {
- if ( ! deployment.zone().equals(job.type().zone())
- && ! controller.applications().isHealthy(new DeploymentId(job.application(), deployment.zone())))
- return true;
- }
- return false;
- }
-
- private void abortIfOutdated(JobStatus job, List<DeploymentStatus.Job> jobs) {
- job.lastTriggered()
- .filter(last -> ! last.hasEnded() && last.reason().reason().isEmpty())
- .ifPresent(last -> {
- if (jobs.stream().noneMatch(versions -> versions.versions().targetsMatch(last.versions())
- && versions.versions().sourcesMatchIfPresent(last.versions()))) {
- String blocked = jobs.stream()
- .map(scheduled -> scheduled.versions().toString())
- .collect(Collectors.joining(", "));
- log.log(Level.INFO, "Aborting outdated run " + last + ", which is blocking runs: " + blocked);
- controller.jobController().abort(last.id(), "run no longer scheduled, and is blocking scheduled runs: " + blocked, false);
- }
- });
- }
-
- /** Returns whether the job is free to start, and also aborts it if it's running with outdated versions. */
- private void abortIfOutdated(DeploymentStatus status, Map<JobId, List<DeploymentStatus.Job>> jobs, JobId job) {
- Readiness readiness = jobs.get(job).get(0).readiness();
- if (readiness.cause() == DelayCause.running)
- abortIfOutdated(status.jobs().get(job).get(), jobs.get(job));
- if (readiness.cause() == DelayCause.blocked && ! job.type().isTest())
- status.jobs().get(new JobId(job.application(), JobType.productionTestOf(job.type().zone())))
- .ifPresent(jobStatus -> abortIfOutdated(jobStatus, jobs.get(jobStatus.id())));
- }
-
- // ---------- Change management o_O ----------
-
- private boolean acceptNewRevision(DeploymentStatus status, InstanceName instance, RevisionId revision) {
- if (status.application().deploymentSpec().instance(instance).isEmpty()) return false; // Unknown instance.
- if (status.application().get(instance).map(Instance::change).map(Change::isRevisionPinned).orElse(false)) return false;
- if ( ! status.jobs().failingWithBrokenRevisionSince(revision, clock.instant().minus(maxFailingRevisionTime))
- .isEmpty()) return false; // Don't deploy a broken revision.
- boolean isChangingRevision = status.application().require(instance).change().revision().isPresent();
- DeploymentInstanceSpec spec = status.application().deploymentSpec().requireInstance(instance);
- Predicate<RevisionId> revisionFilter = spec.revisionTarget() == DeploymentSpec.RevisionTarget.next
- ? failing -> status.application().require(instance).change().revision().get().compareTo(failing) == 0
- : failing -> revision.compareTo(failing) > 0;
- return switch (spec.revisionChange()) {
- case whenClear -> ! isChangingRevision;
- case whenFailing -> ! isChangingRevision || status.hasFailures(revisionFilter);
- case always -> true;
- };
- }
-
- private Instance withRemainingChange(Instance instance, Change change, DeploymentStatus status, boolean allowOutdatedPlatform) {
- Change remaining = change;
- if (status.hasCompleted(instance.name(), change.withoutApplication()))
- remaining = remaining.withoutPlatform();
- if (status.hasCompleted(instance.name(), change.withoutPlatform()))
- remaining = remaining.withoutApplication();
-
- return instance.withChange(status.withPermittedPlatform(remaining, instance.name(), allowOutdatedPlatform));
- }
-
- // ---------- Version and job helpers ----------
-
- private Job deploymentJob(Instance instance, Versions versions, JobType jobType, boolean isNodeAllocationFailure, Instant availableSince, Reason reason) {
- return new Job(instance, versions, jobType, availableSince, isNodeAllocationFailure, instance.change().revision().isPresent(), reason);
- }
-
- // ---------- Data containers ----------
-
-
- private static class Job {
-
- private final ApplicationId instanceId;
- private final JobType jobType;
- private final Versions versions;
- private final Instant availableSince;
- private final boolean isRetry;
- private final boolean isApplicationUpgrade;
- private final Run.Reason reason;
-
- private Job(Instance instance, Versions versions, JobType jobType, Instant availableSince,
- boolean isRetry, boolean isApplicationUpgrade, Run.Reason reason) {
- this.instanceId = instance.id();
- this.jobType = jobType;
- this.versions = versions;
- this.availableSince = availableSince;
- this.isRetry = isRetry;
- this.isApplicationUpgrade = isApplicationUpgrade;
- this.reason = reason;
- }
-
- ApplicationId applicationId() { return instanceId; }
- JobType jobType() { return jobType; }
- Instant availableSince() { return availableSince; } // TODO jvenstad: This is 95% broken now. Change.at() can restore it.
- boolean isRetry() { return isRetry; }
- boolean applicationUpgrade() { return isApplicationUpgrade; }
- Reason reason() { return reason; }
-
- @Override
- public String toString() {
- return jobType + " for " + instanceId +
- " on (" + versions.targetPlatform() + versions.sourcePlatform().map(version -> " <-- " + version).orElse("") +
- ", " + versions.targetRevision() + versions.sourceRevision().map(version -> " <-- " + version).orElse("") +
- "), ready since " + availableSince;
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
deleted file mode 100644
index 9bfa2674754..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunner.java
+++ /dev/null
@@ -1,1063 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import ai.vespa.http.HttpURL;
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.Notifications;
-import com.yahoo.config.application.api.Notifications.When;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.EndpointsChecker;
-import com.yahoo.config.provision.EndpointsChecker.Availability;
-import com.yahoo.config.provision.EndpointsChecker.Status;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateException;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.DeploymentResult;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ServiceConvergence;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentFailureMails;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageStream;
-import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage;
-import com.yahoo.vespa.hosted.controller.maintenance.JobRunner;
-import com.yahoo.vespa.hosted.controller.notification.Notification;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicy;
-import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext;
-import com.yahoo.yolean.Exceptions;
-
-import java.io.ByteArrayOutputStream;
-import java.io.PrintStream;
-import java.io.UncheckedIOException;
-import java.net.InetAddress;
-import java.security.cert.CertificateExpiredException;
-import java.security.cert.CertificateNotYetValidException;
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.config.application.api.Notifications.Role.author;
-import static com.yahoo.config.application.api.Notifications.When.failing;
-import static com.yahoo.config.application.api.Notifications.When.failingCommit;
-import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.active;
-import static com.yahoo.vespa.hosted.controller.api.integration.configserver.Node.State.reserved;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.deploymentFailed;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.error;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.invalidApplication;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.noTests;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.nodeAllocationFailure;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.quotaExceeded;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.reset;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.success;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.testFailure;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.copyVespaLogs;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployInitialReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.report;
-import static com.yahoo.yolean.Exceptions.uncheck;
-import static com.yahoo.yolean.Exceptions.uncheckInterruptedAndRestoreFlag;
-import static java.lang.Math.min;
-import static java.util.Objects.requireNonNull;
-import static java.util.function.Predicate.not;
-import static java.util.logging.Level.FINE;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static java.util.stream.Collectors.joining;
-import static java.util.stream.Collectors.toSet;
-
-/**
- * Runs steps of a deployment job against its provided controller.
- *
- * A dual-purpose logger is set up for each step run here:
- * 1. all messages are logged to a buffer which is stored in an external log storage at the end of execution, and
- * 2. all messages are also logged through the usual logging framework; by default, any messages of level
- * {@code Level.INFO} or higher end up in the Vespa log, and all messages may be sent there by means of log-control.
- *
- * @author jonmv
- */
-public class InternalStepRunner implements StepRunner {
-
- private static final Logger logger = Logger.getLogger(InternalStepRunner.class.getName());
-
- private final Controller controller;
- private final TestConfigSerializer testConfigSerializer;
- private final DeploymentFailureMails mails;
- private final Timeouts timeouts;
-
- public InternalStepRunner(Controller controller) {
- this.controller = controller;
- this.testConfigSerializer = new TestConfigSerializer(controller.system());
- this.mails = new DeploymentFailureMails(controller.serviceRegistry().consoleUrls());
- this.timeouts = Timeouts.of(controller.system());
- }
-
- @Override
- public Optional<RunStatus> run(LockedStep step, RunId id) {
- DualLogger logger = new DualLogger(id, step.get());
- try {
- return switch (step.get()) {
- case deployTester -> deployTester(id, logger);
- case installTester -> installTester(id, logger);
- case deployInitialReal -> deployInitialReal(id, logger);
- case installInitialReal -> installInitialReal(id, logger);
- case deployReal -> deployReal(id, logger);
- case installReal -> installReal(id, logger);
- case startStagingSetup -> startTests(id, true, logger);
- case endStagingSetup -> endTests(id, true, logger);
- case startTests -> startTests(id, false, logger);
- case endTests -> endTests(id, false, logger);
- case copyVespaLogs -> copyVespaLogs(id, logger);
- case deactivateReal -> deactivateReal(id, logger);
- case deactivateTester -> deactivateTester(id, logger);
- case report -> report(id, logger);
- };
- } catch (UncheckedIOException e) {
- logger.logWithInternalException(INFO, "IO exception running " + id + ": " + Exceptions.toMessageString(e), e);
- return Optional.empty();
- } catch (RuntimeException | LinkageError e) {
- logger.log(WARNING, "Unexpected exception running " + id, e);
- if (step.get().alwaysRun() && ! (e instanceof LinkageError)) {
- logger.log("Will keep trying, as this is a cleanup step.");
- return Optional.empty();
- }
- return Optional.of(error);
- }
- }
-
- private Optional<RunStatus> deployInitialReal(RunId id, DualLogger logger) {
- Versions versions = controller.jobController().run(id).versions();
- logger.log("Deploying platform version " +
- versions.sourcePlatform().orElse(versions.targetPlatform()) +
- " and application " +
- versions.sourceRevision().orElse(versions.targetRevision()) + " ...");
- return deployReal(id, true, logger);
- }
-
- private Optional<RunStatus> deployReal(RunId id, DualLogger logger) {
- Versions versions = controller.jobController().run(id).versions();
- logger.log("Deploying platform version " + versions.targetPlatform() +
- " and application " + versions.targetRevision() + " ...");
- return deployReal(id, false, logger);
- }
-
- private Optional<RunStatus> deployReal(RunId id, boolean setTheStage, DualLogger logger) {
- Optional<X509Certificate> testerCertificate = controller.jobController().run(id).testerCertificate();
- return deploy(() -> controller.applications().deploy(id.job(),
- setTheStage,
- logger::log,
- account -> getAndSetCloudAccountWithOverrideForStaging(id, account)),
- controller.jobController().run(id)
- .stepInfo(setTheStage ? deployInitialReal : deployReal).get()
- .startTime().get(),
- id,
- logger)
- .filter(result -> {
- // If no tester cert, or deployment failed, propagate original result.
- if ( ! useTesterCertificate(id) || result != running)
- return true;
- // If tester cert, ensure real is deployed with the tester cert whose key was successfully deployed.
- return controller.jobController().run(id).stepStatus(deployTester).get() == succeeded
- && testerCertificate.equals(controller.jobController().run(id).testerCertificate());
- });
- }
-
- private Optional<RunStatus> deployTester(RunId id, DualLogger logger) {
- Version platform = testerPlatformVersion(id);
- logger.log("Deploying the tester container on platform " + platform + " ...");
- return deploy(() -> controller.applications().deployTester(id.tester(),
- testerPackage(id),
- id.type().zone(),
- platform,
- cloudAccount -> setCloudAccountForStaging(id, cloudAccount)),
- controller.jobController().run(id)
- .stepInfo(deployTester).get()
- .startTime().get(),
- id,
- logger);
- }
-
- private Optional<CloudAccount> setCloudAccountForStaging(RunId id, Optional<CloudAccount> account) {
- if (id.type().environment() == Environment.staging) {
- controller.jobController().locked(id, run -> run.with(account.orElse(CloudAccount.empty)));
- }
- return account;
- }
-
- private Optional<CloudAccount> getAndSetCloudAccountWithOverrideForStaging(RunId id, Optional<CloudAccount> account) {
- if (id.type().environment() == Environment.staging) {
- Instant doom = controller.clock().instant().plusSeconds(60); // Sleeping is bad, but we're already in a sleepy code path: deployment.
- while (true) {
- Run run = controller.jobController().run(id);
- Optional<CloudAccount> stored = run.cloudAccount();
- if (stored.isPresent())
- return stored.filter(not(CloudAccount.empty::equals));
-
- long millisToDoom = Duration.between(controller.clock().instant(), doom).toMillis();
- if (millisToDoom > 0)
- uncheckInterruptedAndRestoreFlag(() -> Thread.sleep(min(millisToDoom, 5000)));
- else
- throw new CloudAccountNotSetException("Cloud account not yet set; must deploy tests first");
- }
- }
- account.ifPresent(cloudAccount -> controller.jobController().locked(id, run -> run.with(cloudAccount)));
- return account;
- }
-
- private Optional<RunStatus> deploy(Supplier<DeploymentResult> deployment, Instant startTime, RunId id, DualLogger logger) {
- try {
- DeploymentResult result = deployment.get();
- logger.logAll(result.log().stream()
- .map(entry -> new LogEntry(0, // Sequenced by BufferedLogStore.
- Instant.ofEpochMilli(entry.epochMillis()),
- LogEntry.typeOf(entry.level()),
- entry.message()))
- .toList());
-
- logger.log("Deployment successful.");
- logger.log(result.message());
-
- return Optional.of(running);
- }
- catch (ConfigServerException e) {
- // Retry certain failures for up to one hour.
- Optional<RunStatus> result = startTime.isBefore(controller.clock().instant().minus(Duration.ofHours(1)))
- ? Optional.of(deploymentFailed) : Optional.empty();
- if (result.isPresent())
- logger.log(WARNING, "Deployment failed for one hour; giving up now!");
-
- switch (e.code()) {
- case CERTIFICATE_NOT_READY -> {
- logger.log("No valid CA signed certificate for app available to config server");
- if (startTime.plus(timeouts.endpointCertificate()).isBefore(controller.clock().instant())) {
- logger.log(WARNING, "CA signed certificate for app not available to config server within " +
- timeouts.endpointCertificate().toMinutes() + " minutes");
- return Optional.of(RunStatus.endpointCertificateTimeout);
- }
- return result;
- }
- case ACTIVATION_CONFLICT, APPLICATION_LOCK_FAILURE, CONFIG_NOT_CONVERGED -> {
- logger.log("Deployment failed with possibly transient error " + e.code() +
- ", will retry: " + e.getMessage());
- return result;
- }
- case INTERNAL_SERVER_ERROR -> {
- // Log only error code, to avoid exposing internal data in error message
- logger.log("Deployment failed with possibly transient error " + e.code() + ", will retry");
- return result;
- }
- case LOAD_BALANCER_NOT_READY, PARENT_HOST_NOT_READY -> {
- logger.log(e.message()); // Consider splitting these messages in summary and details, on config server.
- Instant someTimeAfterStart = startTime.plusSeconds(200);
- if (someTimeAfterStart.isAfter(controller.clock().instant()))
- controller.jobController().locked(id, run -> run.sleepingUntil(someTimeAfterStart));
- return result;
- }
- case NODE_ALLOCATION_FAILURE -> {
- logger.log(e.message());
- return controller.system().isCd() && startTime.plus(timeouts.capacity()).isAfter(controller.clock().instant())
- ? result
- : Optional.of(nodeAllocationFailure);
- }
- case INVALID_APPLICATION_PACKAGE -> {
- logger.log(WARNING, e.getMessage());
- return Optional.of(invalidApplication);
- }
- case BAD_REQUEST -> {
- logger.log(WARNING, e.getMessage());
- return Optional.of(deploymentFailed);
- }
- case QUOTA_EXCEEDED -> {
- logger.log(WARNING, e.getMessage());
- return Optional.of(quotaExceeded);
- }
- }
-
- throw e;
- }
- catch (CloudAccountNotSetException e) {
- logger.log(INFO, "Timed out waiting for cloud account to be set for " + id + ": " + e.getMessage());
- return Optional.empty();
- }
- catch (IllegalArgumentException e) {
- logger.log(WARNING, e.getMessage());
- return Optional.of(deploymentFailed);
- }
- catch (EndpointCertificateException e) {
- switch (e.type()) {
- case CERT_NOT_AVAILABLE:
- // Same as CERTIFICATE_NOT_READY above, only from the controller
- logger.log("Retrieving CA signed certificate for the application. " +
- "This may take up to " + timeouts.endpointCertificate().toMinutes() + " minutes on first deployment.");
- if (startTime.plus(timeouts.endpointCertificate()).isBefore(controller.clock().instant())) {
- logger.log(WARNING, "CA signed certificate for app not available within " +
- timeouts.endpointCertificate().toMinutes() + " minutes: " + Exceptions.toMessageString(e));
- return Optional.of(RunStatus.endpointCertificateTimeout);
- }
- return Optional.empty();
- default:
- throw e; // Should be surfaced / fail deployment
- }
- }
- }
-
- private Optional<RunStatus> installInitialReal(RunId id, DualLogger logger) {
- return installReal(id, true, logger);
- }
-
- private Optional<RunStatus> installReal(RunId id, DualLogger logger) {
- return installReal(id, false, logger);
- }
-
- private Optional<RunStatus> installReal(RunId id, boolean setTheStage, DualLogger logger) {
- Optional<Deployment> deployment = deployment(id.application(), id.type());
- if (deployment.isEmpty()) {
- logger.log("Deployment expired before installation was successful.");
- return Optional.of(installationFailed);
- }
-
- Versions versions = controller.jobController().run(id).versions();
- Version platform = setTheStage ? versions.sourcePlatform().orElse(versions.targetPlatform()) : versions.targetPlatform();
-
- Run run = controller.jobController().run(id);
- // In manually deployed zones it is allowed for some model versions not being built (e.g due to incompatibility)
- // but deployment still succeeding, so we cannot use version when checking for config convergence
- Optional<Version> platformVersion = id.type().environment().isManuallyDeployed() ? Optional.empty() : Optional.of(platform);
- Optional<ServiceConvergence> services = configServer().serviceConvergence(new DeploymentId(id.application(), id.type().zone()),
- platformVersion);
- if (services.isEmpty()) {
- logger.log("Config status not currently available -- will retry.");
- return Optional.empty();
- }
- List<Node> nodes = configServer().nodeRepository().list(id.type().zone(),
- NodeFilter.all()
- .applications(id.application())
- .states(active));
-
- Set<HostName> parentHostnames = nodes.stream().map(node -> node.parentHostname().get()).collect(toSet());
- List<Node> parents = configServer().nodeRepository().list(id.type().zone(),
- NodeFilter.all()
- .hostnames(parentHostnames));
- boolean firstTick = run.convergenceSummary().isEmpty();
- NodeList nodeList = NodeList.of(nodes, parents, services.get());
- ConvergenceSummary summary = nodeList.summary();
- if (firstTick) { // Run the first time (for each convergence step).
- logger.log("######## Details for all nodes ########");
- logger.log(nodeList.asList().stream()
- .flatMap(node -> nodeDetails(node, true))
- .toList());
- }
- else if ( ! summary.converged()) {
- logger.log("Waiting for convergence of " + summary.services() + " services across " + summary.nodes() + " nodes");
- if (summary.needPlatformUpgrade() > 0)
- logger.log(summary.upgradingPlatform() + "/" + summary.needPlatformUpgrade() + " nodes upgrading platform");
- if (summary.needReboot() > 0)
- logger.log(summary.rebooting() + "/" + summary.needReboot() + " nodes rebooting");
- if (summary.needRestart() > 0)
- logger.log(summary.restarting() + "/" + summary.needRestart() + " nodes restarting");
- if (summary.retiring() > 0)
- logger.log(summary.retiring() + " nodes retiring");
- if (summary.upgradingFirmware() > 0)
- logger.log(summary.upgradingFirmware() + " nodes upgrading firmware");
- if (summary.upgradingOs() > 0)
- logger.log(summary.upgradingOs() + " nodes upgrading OS");
- if (summary.needNewConfig() > 0)
- logger.log(summary.needNewConfig() + " application services still deploying");
- }
- if (summary.converged()) {
- controller.jobController().locked(id, lockedRun -> lockedRun.withSummary(null));
- Availability availability = endpointsAvailable(id.application(), id.type().zone(), deployment.get(), run.versions().sourceRevision().isEmpty(), logger);
- if (availability.status() == Status.available) {
- if (controller.routing().policies().processDnsChallenges(new DeploymentId(id.application(), id.type().zone()))) {
- logger.log("Installation succeeded!");
- return Optional.of(running);
- }
- logger.log("Waiting for DNS challenges for private endpoints to be processed");
- return Optional.empty();
- }
- logger.log(availability.message());
- if (availability.status() == Status.endpointsUnavailable && timedOut(id, deployment.get(), timeouts.endpoint())) {
- logger.log(WARNING, "Endpoints failed to show up within " + timeouts.endpoint().toMinutes() + " minutes!");
- return Optional.of(error);
- }
- }
-
- String failureReason = null;
-
- NodeList suspendedTooLong = nodeList.isStateful()
- .suspendedSince(controller.clock().instant().minus(timeouts.statefulNodesDown()))
- .and(nodeList.not().isStateful()
- .suspendedSince(controller.clock().instant().minus(timeouts.statelessNodesDown()))
- );
- if ( ! suspendedTooLong.isEmpty() && deployment.get().at().plus(timeouts.statelessNodesDown()).isBefore(controller.clock().instant())) {
- failureReason = "Some nodes have been suspended for more than the allowed threshold:\n" +
- suspendedTooLong.asList().stream().map(node -> node.node().hostname().value()).collect(joining("\n"));
- }
-
- if (run.noNodesDownSince()
- .map(since -> since.isBefore(controller.clock().instant().minus(timeouts.noNodesDown())))
- .orElse(false)) {
- if (summary.needPlatformUpgrade() > 0 || summary.needReboot() > 0 || summary.needRestart() > 0)
- failureReason = "Timed out after waiting " + timeouts.noNodesDown().toMinutes() + " minutes for " +
- "nodes to suspend. This is normal if the cluster is excessively busy. " +
- "Nodes will continue to attempt suspension to progress installation independently of " +
- "this run.";
- else
- failureReason = "Nodes not able to start with new application package.";
- }
-
- Duration timeout = JobRunner.jobTimeout.minusHours(1); // Time out before job dies.
- if (timedOut(id, deployment.get(), timeout)) {
- failureReason = "Installation failed to complete within " + timeout.toHours() + "hours!";
- }
-
- if (failureReason != null) {
- logger.log("######## Details for all nodes ########");
- logger.log(nodeList.asList().stream()
- .flatMap(node -> nodeDetails(node, true))
- .toList());
- logger.log("######## Details for nodes with pending changes ########");
- logger.log(nodeList.not().in(nodeList.not().needsNewConfig()
- .not().needsPlatformUpgrade()
- .not().needsReboot()
- .not().needsRestart()
- .not().needsFirmwareUpgrade()
- .not().needsOsUpgrade())
- .asList().stream()
- .flatMap(node -> nodeDetails(node, true))
- .toList());
- logger.log(INFO, failureReason);
- return Optional.of(installationFailed);
- }
-
- if ( ! firstTick)
- logger.log(FINE, nodeList.expectedDown().and(nodeList.needsNewConfig()).asList().stream()
- .distinct()
- .flatMap(node -> nodeDetails(node, false))
- .toList());
-
- controller.jobController().locked(id, lockedRun -> {
- Instant noNodesDownSince = nodeList.allowedDown().isEmpty() ? lockedRun.noNodesDownSince().orElse(controller.clock().instant()) : null;
- return lockedRun.noNodesDownSince(noNodesDownSince).withSummary(summary);
- });
-
- return Optional.empty();
- }
-
- private Version testerPlatformVersion(RunId id) {
- Version targetPlatform = controller.jobController().run(id).versions().targetPlatform();
- Version systemVersion = controller.readSystemVersion();
- boolean incompatible = controller.applications().versionCompatibility(id.application()).refuse(targetPlatform, systemVersion);
- return incompatible || application(id.application()).change().isPlatformPinned() ? targetPlatform : systemVersion;
- }
-
- private Optional<RunStatus> installTester(RunId id, DualLogger logger) {
- Run run = controller.jobController().run(id);
- Version platform = testerPlatformVersion(id);
- ZoneId zone = id.type().zone();
- ApplicationId testerId = id.tester().id();
-
- Optional<ServiceConvergence> services = configServer().serviceConvergence(new DeploymentId(testerId, zone),
- Optional.of(platform));
- if (services.isEmpty()) {
- if (run.stepInfo(installTester).get().startTime().get().isBefore(controller.clock().instant().minus(Duration.ofMinutes(30)))) {
- logger.log(WARNING, "Config status not available after 30 minutes; giving up!");
- return Optional.of(error);
- }
- else {
- logger.log("Config status not currently available -- will retry.");
- return Optional.empty();
- }
- }
- List<Node> nodes = configServer().nodeRepository().list(zone,
- NodeFilter.all()
- .applications(testerId)
- .states(active, reserved));
- Set<HostName> parentHostnames = nodes.stream().map(node -> node.parentHostname().get()).collect(toSet());
- List<Node> parents = configServer().nodeRepository().list(zone,
- NodeFilter.all()
- .hostnames(parentHostnames));
- NodeList nodeList = NodeList.of(nodes, parents, services.get());
- logger.log(nodeList.asList().stream()
- .flatMap(node -> nodeDetails(node, false))
- .toList());
-
- if (nodeList.summary().converged() && testerContainersAreUp(testerId, zone, logger)) {
- logger.log("Tester container successfully installed!");
- return Optional.of(running);
- }
-
- if (run.stepInfo(installTester).get().startTime().get().plus(timeouts.tester()).isBefore(controller.clock().instant())) {
- logger.log(WARNING, "Installation of tester failed to complete within " + timeouts.tester().toMinutes() + " minutes!");
- return Optional.of(error);
- }
-
- return Optional.empty();
- }
-
- private ConfigServer configServer() { return controller.serviceRegistry().configServer(); }
-
- /** Returns true iff all containers in the tester deployment give 100 consecutive 200 OK responses on /status.html. */
- private boolean testerContainersAreUp(ApplicationId id, ZoneId zoneId, DualLogger logger) {
- DeploymentId deploymentId = new DeploymentId(id, zoneId);
- if (controller.jobController().cloud().testerReady(deploymentId)) {
- return true;
- } else {
- logger.log("Failed to get 100 consecutive OKs from tester container for " + deploymentId);
- return false;
- }
- }
-
- private Availability endpointsAvailable(ApplicationId id, ZoneId zone, Deployment deployment, boolean initialDeployment, DualLogger logger) {
- DeploymentId deploymentId = new DeploymentId(id, zone);
- Map<ZoneId, List<Endpoint>> endpoints = controller.routing().readStepRunnerEndpointsOf(Set.of(deploymentId));
- logEndpoints(endpoints, logger);
- DeploymentRoutingContext context = controller.routing().of(deploymentId);
- boolean resolveEndpoints = context.routingMethod() == RoutingMethod.exclusive;
- return controller.serviceRegistry().testerCloud().verifyEndpoints(
- deploymentId,
- endpoints.getOrDefault(zone, List.of())
- .stream()
- .map(endpoint -> {
- ClusterSpec.Id cluster = ClusterSpec.Id.from(endpoint.name());
- RoutingPolicy policy = context.routingPolicy(cluster).get();
- return new EndpointsChecker.Endpoint(id,
- cluster,
- HttpURL.from(endpoint.url()),
- policy.ipAddress().filter(__ -> resolveEndpoints).map(uncheck(InetAddress::getByName)),
- policy.canonicalName().filter(__ -> resolveEndpoints),
- policy.isPublic(),
- deployment.cloudAccount());
- }).toList(),
- initialDeployment);
- }
-
- private void logEndpoints(Map<ZoneId, List<Endpoint>> zoneEndpoints, DualLogger logger) {
- List<String> messages = new ArrayList<>();
- messages.add("Found endpoints:");
- zoneEndpoints.forEach((zone, endpoints) -> {
- messages.add("- " + zone);
- for (Endpoint endpoint : endpoints)
- messages.add(" |-- " + endpoint.url() + " (cluster '" + endpoint.name() + "')");
- });
- logger.log(messages);
- }
-
- private Stream<String> nodeDetails(NodeWithServices node, boolean printAllServices) {
- return Stream.concat(Stream.of(node.node().hostname() + ": " + humanize(node.node().serviceState()) + (node.node().suspendedSince().map(since -> " since " + since).orElse("")),
- "--- platform " + wantedPlatform(node.node()) + (node.needsPlatformUpgrade()
- ? " <-- " + currentPlatform(node.node())
- : "") +
- (node.needsOsUpgrade() && node.isAllowedDown()
- ? ", upgrading OS (" + node.parent().wantedOsVersion() + " <-- " + node.parent().currentOsVersion() + ")"
- : "") +
- (node.needsFirmwareUpgrade() && node.isAllowedDown()
- ? ", upgrading firmware"
- : "") +
- (node.needsRestart()
- ? ", restart pending (" + node.node().wantedRestartGeneration() + " <-- " + node.node().restartGeneration() + ")"
- : "") +
- (node.needsReboot()
- ? ", reboot pending (" + node.node().wantedRebootGeneration() + " <-- " + node.node().rebootGeneration() + ")"
- : "")),
- node.services().stream()
- .filter(service -> printAllServices || node.needsNewConfig())
- .map(service -> "--- " + service.type() + " on port " + service.port() + (service.currentGeneration() == -1
- ? " has not started "
- : " has config generation " + service.currentGeneration() + ", wanted is " + node.wantedConfigGeneration())));
- }
-
-
- private String wantedPlatform(Node node) {
- return node.wantedDockerImage().repository() + ":" + node.wantedVersion();
- }
-
- private String currentPlatform(Node node) {
- String currentRepo = node.currentDockerImage().repository();
- String wantedRepo = node.wantedDockerImage().repository();
- return (currentRepo.equals(wantedRepo) ? "" : currentRepo + ":") + node.currentVersion();
- }
-
- private String humanize(Node.ServiceState state) {
- switch (state) {
- case allowedDown: return "allowed to be DOWN";
- case expectedUp: return "expected to be UP";
- case permanentlyDown: return "permanently DOWN";
- case unorchestrated: return "unorchestrated";
- default: return state.name();
- }
- }
-
- private Optional<RunStatus> startTests(RunId id, boolean isSetup, DualLogger logger) {
- Optional<Deployment> deployment = deployment(id.application(), id.type());
- if (deployment.isEmpty()) {
- logger.log(INFO, "Deployment expired before tests could start.");
- return Optional.of(error);
- }
-
- var deployments = controller.applications().requireInstance(id.application())
- .productionDeployments().keySet().stream()
- .map(zone -> new DeploymentId(id.application(), zone))
- .collect(Collectors.toSet());
- ZoneId zoneId = id.type().zone();
- deployments.add(new DeploymentId(id.application(), zoneId));
-
- logger.log("Attempting to find endpoints ...");
- var endpoints = controller.routing().readStepRunnerEndpointsOf(deployments);
- if ( ! endpoints.containsKey(zoneId)) {
- logger.log(WARNING, "Endpoints for the deployment to test vanished again, while it was still active!");
- return Optional.of(error);
- }
- logEndpoints(endpoints, logger);
-
- if (!controller.jobController().cloud().testerReady(getTesterDeploymentId(id))) {
- logger.log(WARNING, "Tester container went bad!");
- return Optional.of(error);
- }
-
- logger.log("Starting tests ...");
- TesterCloud.Suite suite = TesterCloud.Suite.of(id.type(), isSetup);
- byte[] config = testConfigSerializer.configJson(id.application(),
- id.type(),
- true,
- deployment.get().version(),
- deployment.get().revision(),
- deployment.get().at(),
- endpoints,
- controller.applications().reachableContentClustersByZone(deployments));
- controller.jobController().cloud().startTests(getTesterDeploymentId(id), suite, config);
- return Optional.of(running);
- }
-
- @SuppressWarnings("fallthrough")
- private Optional<RunStatus> endTests(RunId id, boolean isSetup, DualLogger logger) {
- Optional<Deployment> deployment = deployment(id.application(), id.type());
- if (deployment.isEmpty()) {
- logger.log(INFO, "Deployment expired before tests could complete.");
- return Optional.of(error);
- }
-
- Optional<X509Certificate> testerCertificate = controller.jobController().run(id).testerCertificate();
- if (testerCertificate.isPresent()) {
- try {
- testerCertificate.get().checkValidity(Date.from(controller.clock().instant()));
- }
- catch (CertificateExpiredException | CertificateNotYetValidException e) {
- logger.log(WARNING, "Tester certificate expired before tests could complete.");
- return Optional.of(error);
- }
- }
-
- controller.jobController().updateTestLog(id);
-
- TesterCloud.Status testStatus = controller.jobController().cloud().getStatus(getTesterDeploymentId(id));
- switch (testStatus) {
- case NOT_STARTED:
- throw new IllegalStateException("Tester reports tests not started, even though they should have!");
- case RUNNING:
- return Optional.empty();
- case FAILURE:
- logger.log("Tests failed.");
- controller.jobController().updateTestReport(id);
- return Optional.of(testFailure);
- case INCONCLUSIVE:
- controller.jobController().updateTestReport(id);
- controller.jobController().locked(id, run -> {
- Instant nextAttemptAt = run.start();
- while ( ! nextAttemptAt.isAfter(controller.clock().instant())) nextAttemptAt = nextAttemptAt.plusSeconds(1800);
- logger.log("Tests were inconclusive, and will run again at " + nextAttemptAt + ".");
- return run.sleepingUntil(nextAttemptAt);
- });
- return Optional.of(reset);
- case ERROR:
- logger.log(INFO, "Tester failed running its tests!");
- controller.jobController().updateTestReport(id);
- return Optional.of(error);
- case NO_TESTS:
- if ( ! isSetup) { // TODO: consider changing this Later™
- TesterCloud.Suite suite = TesterCloud.Suite.of(id.type(), isSetup);
- logger.log(INFO, "No tests were found in the test package, for test suite '" + suite + "'");
- logger.log(INFO, "The test package should either contain basic HTTP tests under 'tests/<suite-name>/', " +
- "or a Java test bundle under 'components/' with at least one test with the annotation " +
- "for this suite. See docs.vespa.ai/en/testing.html for details.");
- controller.jobController().updateTestReport(id);
-
- DeploymentSpec spec = controller.applications().requireApplication(TenantAndApplicationId.from(id.application())).deploymentSpec();
- boolean requireTests = spec.steps().stream().anyMatch(step -> step.concerns(id.type().environment()));
- logger.log(WARNING, "No tests were actually run, but this test suite is explicitly declared in 'deployment.xml'. " +
- "Either add tests, ensure they're correctly configured, or remove the test declaration.");
- return Optional.of(requireTests ? testFailure : noTests);
- }
- case SUCCESS:
- logger.log("Tests completed successfully.");
- controller.jobController().updateTestReport(id);
- return Optional.of(running);
- default:
- throw new IllegalStateException("Unknown status '" + testStatus + "'!");
- }
- }
-
- private Optional<RunStatus> copyVespaLogs(RunId id, DualLogger logger) {
- if (deployment(id.application(), id.type()).isPresent())
- try {
- controller.jobController().updateVespaLog(id);
- }
- // Hitting a config server which doesn't have this particular app loaded causes a 404.
- catch (ConfigServerException e) {
- Instant doom = controller.jobController().run(id).stepInfo(copyVespaLogs).get().startTime().get()
- .plus(Duration.ofMinutes(3));
- if (e.code() == ConfigServerException.ErrorCode.NOT_FOUND && controller.clock().instant().isBefore(doom)) {
- logger.log(INFO, "Found no logs, but will retry");
- return Optional.empty();
- }
- else {
- logger.log(INFO, "Failure getting vespa logs for " + id, e);
- return Optional.of(error);
- }
- }
- catch (Exception e) {
- logger.log(INFO, "Failure getting vespa logs for " + id, e);
- return Optional.of(error);
- }
- return Optional.of(running);
- }
-
- private Optional<RunStatus> deactivateReal(RunId id, DualLogger logger) {
- try {
- logger.log("Deactivating deployment of " + id.application() + " in " + id.type().zone() + " ...");
- controller.applications().deactivate(id.application(), id.type().zone());
- return Optional.of(running);
- }
- catch (RuntimeException e) {
- logger.log(WARNING, "Failed deleting application " + id.application(), e);
- Instant startTime = controller.jobController().run(id).stepInfo(deactivateReal).get().startTime().get();
- return startTime.isBefore(controller.clock().instant().minus(Duration.ofHours(1)))
- ? Optional.of(error)
- : Optional.empty();
- }
- }
-
- private Optional<RunStatus> deactivateTester(RunId id, DualLogger logger) {
- try {
- logger.log("Deactivating tester of " + id.application() + " in " + id.type().zone() + " ...");
- controller.jobController().deactivateTester(id.tester(), id.type());
- return Optional.of(running);
- }
- catch (RuntimeException e) {
- logger.log(WARNING, "Failed deleting tester of " + id.application(), e);
- Instant startTime = controller.jobController().run(id).stepInfo(deactivateTester).get().startTime().get();
- return startTime.isBefore(controller.clock().instant().minus(Duration.ofHours(1)))
- ? Optional.of(error)
- : Optional.empty();
- }
- }
-
- private Optional<RunStatus> report(RunId id, DualLogger logger) {
- try {
- boolean isRemoved = ! id.type().environment().isManuallyDeployed()
- && ! controller.jobController().deploymentStatus(controller.applications().requireApplication(TenantAndApplicationId.from(id.application())))
- .jobSteps().containsKey(id.job());
-
- controller.jobController().active(id).ifPresent(run -> {
- if (run.status() == reset)
- return;
-
- if (run.hasFailed() && ! isRemoved)
- sendEmailNotification(run, logger);
-
- updateConsoleNotification(run, isRemoved);
- });
- }
- catch (IllegalStateException e) {
- logger.log(INFO, "Job '" + id.type() + "' no longer supposed to run?", e);
- return Optional.of(error);
- }
- catch (RuntimeException e) {
- Instant start = controller.jobController().run(id).stepInfo(report).get().startTime().get();
- return (controller.clock().instant().isAfter(start.plusSeconds(180)))
- ? Optional.empty()
- : Optional.of(error);
- }
- return Optional.of(running);
- }
-
- /** Sends a mail with a notification of a failed run, if one should be sent. */
- private void sendEmailNotification(Run run, DualLogger logger) {
- if ( ! isNewFailure(run))
- return;
-
- Application application = controller.applications().requireApplication(TenantAndApplicationId.from(run.id().application()));
- Notifications notifications = application.deploymentSpec().requireInstance(run.id().application().instance()).notifications();
- boolean newCommit = application.require(run.id().application().instance()).change().revision()
- .map(run.versions().targetRevision()::equals)
- .orElse(false);
- When when = newCommit ? failingCommit : failing;
-
- List<String> recipients = new ArrayList<>(notifications.emailAddressesFor(when));
- if (notifications.emailRolesFor(when).contains(author))
- application.revisions().get(run.versions().targetRevision()).authorEmail().ifPresent(recipients::add);
-
- if (recipients.isEmpty())
- return;
-
- try {
- logger.log(INFO, "Sending failure notification to " + String.join(", ", recipients));
- mailOf(run, recipients).ifPresent(controller.serviceRegistry().mailer()::send);
- }
- catch (RuntimeException e) {
- logger.log(WARNING, "Exception trying to send mail for " + run.id(), e);
- }
- }
-
- private boolean isNewFailure(Run run) {
- return controller.jobController().lastCompleted(run.id().job())
- .map(previous -> ! previous.hasFailed() || ! previous.versions().targetsMatch(run.versions()))
- .orElse(true);
- }
-
- private void updateConsoleNotification(Run run, boolean isRemoved) {
- NotificationSource source = NotificationSource.from(run.id());
- Consumer<String> updater = msg -> controller.notificationsDb().setDeploymentNotification(run.id(), msg);
- switch (isRemoved ? success : run.status()) {
- case aborted, cancelled: return; // wait and see how the next run goes.
- case noTests:
- case running:
- case success:
- controller.notificationsDb().removeNotification(source, Notification.Type.deployment);
- return;
- case nodeAllocationFailure:
- if ( ! run.id().type().environment().isTest()) updater.accept("could not allocate the requested capacity to your tenant. Please contact Vespa Cloud support.");
- return;
- case invalidApplication:
- updater.accept("invalid application configuration. Please review warnings and errors in the deployment job log.");
- return;
- case deploymentFailed:
- updater.accept("failure processing application configuration. Please review warnings and errors in the deployment job log.");
- return;
- case installationFailed:
- updater.accept("nodes were not able to deploy to the new configuration. Please check the Vespa log for errors, and contact Vespa Cloud support if unable to resolve these.");
- return;
- case testFailure:
- updater.accept("one or more verification tests against the deployment failed. Please review test output in the deployment job log.");
- return;
- case error:
- case endpointCertificateTimeout:
- break;
- case quotaExceeded:
- updater.accept("quota exceeded. Contact support to upgrade your plan.");
- return;
- default:
- logger.log(WARNING, "Don't know what to set console notification to for run status '" + run.status() + "'");
- }
- updater.accept("something in the deployment framework went wrong. Such errors are " +
- "usually transient. Please contact Vespa Cloud support if the problem persists.");
- }
-
- private Optional<Mail> mailOf(Run run, List<String> recipients) {
- switch (run.status()) {
- case running:
- case aborted:
- case cancelled:
- case noTests:
- case success:
- return Optional.empty();
- case nodeAllocationFailure:
- return run.id().type().isProduction() ? Optional.of(mails.nodeAllocationFailure(run.id(), recipients)) : Optional.empty();
- case deploymentFailed:
- case invalidApplication:
- return Optional.of(mails.deploymentFailure(run.id(), recipients));
- case installationFailed:
- return Optional.of(mails.installationFailure(run.id(), recipients));
- case testFailure:
- return Optional.of(mails.testFailure(run.id(), recipients));
- case error:
- case endpointCertificateTimeout:
- break;
- default:
- logger.log(WARNING, "Don't know what mail to send for run status '" + run.status() + "'");
- }
- return Optional.of(mails.systemError(run.id(), recipients));
- }
-
- /** Returns the deployment of the real application in the zone of the given job, if it exists. */
- private Optional<Deployment> deployment(ApplicationId id, JobType type) {
- return Optional.ofNullable(application(id).deployments().get(type.zone()));
- }
-
- /** Returns the real application with the given id. */
- private Instance application(ApplicationId id) {
- controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), __ -> { }); // Memory fence.
- return controller.applications().requireInstance(id);
- }
-
- /**
- * Returns whether the time since deployment is more than the zone deployment expiry, or the given timeout.
- *
- * We time out the job before the deployment expires, for zones where deployments are not persistent,
- * to be able to collect the Vespa log from the deployment. Thus, the lower of the zone's deployment expiry,
- * and the given default installation timeout, minus one minute, is used as a timeout threshold.
- */
- private boolean timedOut(RunId id, Deployment deployment, Duration defaultTimeout) {
- // TODO jonmv: This is a workaround for new deployment writes not yet being visible in spite of Curator locking.
- // TODO Investigate what's going on here, and remove this workaround.
- Run run = controller.jobController().run(id);
- if ( ! controller.system().isCd() && run.start().isAfter(deployment.at()))
- return false;
-
- Duration timeout = controller.zoneRegistry().getDeploymentTimeToLive(deployment.zone())
- .filter(zoneTimeout -> zoneTimeout.compareTo(defaultTimeout) < 0)
- .orElse(defaultTimeout);
- return deployment.at().isBefore(controller.clock().instant().minus(timeout.minus(Duration.ofMinutes(1))));
- }
-
- private boolean useTesterCertificate(RunId id) {
- return controller.system().isPublic() && id.type().environment().isTest();
- }
-
- /** Returns the application package for the tester application, assembled from a generated config, fat-jar and services.xml. */
- private ApplicationPackageStream testerPackage(RunId id) {
- RevisionId revision = controller.jobController().run(id).versions().targetRevision();
- DeploymentSpec spec = controller.applications().requireApplication(TenantAndApplicationId.from(id.application())).deploymentSpec();
- boolean useTesterCertificate = useTesterCertificate(id);
-
- TestPackage testPackage = new TestPackage(() -> controller.applications().applicationStore().streamTester(id.application().tenant(),
- id.application().application(), revision),
- controller.system().isPublic(),
- controller.zoneRegistry().get(id.type().zone()).getCloudName(),
- id,
- controller.controllerConfig().steprunner().testerapp(),
- spec,
- useTesterCertificate ? controller.clock().instant() : null,
- timeouts.testerCertificate());
- if (useTesterCertificate) controller.jobController().storeTesterCertificate(id, testPackage.certificate());
-
- return testPackage.asApplicationPackage();
- }
-
- private DeploymentId getTesterDeploymentId(RunId runId) {
- ZoneId zoneId = runId.type().zone();
- return new DeploymentId(runId.tester().id(), zoneId);
- }
-
- /** Logger which logs to a {@link JobController}, as well as to the parent class' {@link Logger}. */
- private class DualLogger {
-
- private final RunId id;
- private final Step step;
-
- private DualLogger(RunId id, Step step) {
- this.id = id;
- this.step = step;
- }
-
- private void log(String... messages) {
- log(INFO, List.of(messages));
- }
-
- private void log(Level level, String... messages) {
- log(level, List.of(messages));
- }
-
- private void logAll(List<LogEntry> messages) {
- controller.jobController().log(id, step, messages);
- }
-
- private void log(List<String> messages) {
- log(INFO, messages);
- }
-
- private void log(Level level, List<String> messages) {
- controller.jobController().log(id, step, level, messages);
- }
-
- private void log(Level level, String message) {
- log(level, message, null);
- }
-
- // Print stack trace in our logs, but don't expose it to end users
- private void logWithInternalException(Level level, String message, Throwable thrown) {
- logger.log(level, id + " at " + step + ": " + message, thrown);
- controller.jobController().log(id, step, level, message);
- }
-
- private void log(Level level, String message, Throwable thrown) {
- logger.log(level, id + " at " + step + ": " + message, thrown);
-
- if (thrown != null) {
- ByteArrayOutputStream traceBuffer = new ByteArrayOutputStream();
- thrown.printStackTrace(new PrintStream(traceBuffer));
- message += "\n" + traceBuffer;
- }
- controller.jobController().log(id, step, level, message);
- }
-
- }
-
-
- static class Timeouts {
-
- private final SystemName system;
-
- private Timeouts(SystemName system) {
- this.system = requireNonNull(system);
- }
-
- public static Timeouts of(SystemName system) {
- return new Timeouts(system);
- }
-
- Duration capacity() { return Duration.ofMinutes(system.isCd() ? 15 : 0); }
- Duration endpoint() { return Duration.ofMinutes(15); }
- Duration endpointCertificate() { return Duration.ofMinutes(20); }
- Duration tester() { return Duration.ofMinutes(30); }
- Duration statelessNodesDown() { return Duration.ofMinutes(system.isCd() ? 30 : 60); }
- Duration statefulNodesDown() { return Duration.ofMinutes(system.isCd() ? 30 : 720); }
- Duration noNodesDown() { return Duration.ofMinutes(system.isCd() ? 30 : 240); }
- Duration testerCertificate() { return Duration.ofMinutes(300); }
-
- }
-
- private static class CloudAccountNotSetException extends RuntimeException {
- CloudAccountNotSetException(String message) { super(message); }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
deleted file mode 100644
index ae6bcdea00c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobController.java
+++ /dev/null
@@ -1,932 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.google.common.collect.ImmutableSortedMap;
-import com.yahoo.component.Version;
-import com.yahoo.component.VersionCompatibility;
-import com.yahoo.concurrent.UncheckedTimeoutException;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.flags.FetchVector.Dimension;
-import com.yahoo.vespa.flags.ListFlag;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.LockedApplication;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageDiff;
-import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage;
-import com.yahoo.vespa.hosted.controller.deployment.Run.Reason;
-import com.yahoo.vespa.hosted.controller.notification.Notification.Type;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import com.yahoo.vespa.hosted.controller.persistence.BufferedLogStore;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Deque;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.NavigableMap;
-import java.util.NoSuchElementException;
-import java.util.Optional;
-import java.util.OptionalLong;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-import java.util.function.UnaryOperator;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Stream;
-
-import static com.yahoo.collections.Iterables.reversed;
-import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.deploymentFile;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.aborted;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.cancelled;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.reset;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.copyVespaLogs;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.endStagingSetup;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.endTests;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installInitialReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.report;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.time.temporal.ChronoUnit.SECONDS;
-import static java.util.Comparator.comparing;
-import static java.util.Comparator.naturalOrder;
-import static java.util.function.Predicate.not;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-
-/**
- * A singleton owned by the controller, which contains the state and methods for controlling deployment jobs.
- *
- * Keys are the {@link ApplicationId} of the real application, for which the deployment job is run, the
- * {@link JobType} to run, and the strictly increasing run number of this combination.
- * The deployment jobs run tests using regular applications, but these tester application IDs are not to be used elsewhere.
- *
- * Jobs consist of sets of {@link Step}s, defined in {@link JobProfile}s.
- * Each run is represented by a {@link Run}, which holds the status of each step of the run, as well as
- * some other meta data.
- *
- * @author jonmv
- */
-public class JobController {
-
- public static final Duration maxHistoryAge = Duration.ofDays(60);
- public static final Duration obsoletePackageExpiry = Duration.ofDays(7);
-
- private static final Logger log = Logger.getLogger(JobController.class.getName());
-
- private final int historyLength;
- private final Controller controller;
- private final CuratorDb curator;
- private final BufferedLogStore logs;
- private final TesterCloud cloud;
- private final JobMetrics metric;
- private final ListFlag<String> disabledZones;
-
- private final AtomicReference<Consumer<Run>> runner = new AtomicReference<>(__ -> { });
-
- public JobController(Controller controller) {
- this.historyLength = controller.system().isCd() ? 256 : 64;
- this.controller = controller;
- this.curator = controller.curator();
- this.logs = new BufferedLogStore(curator, controller.serviceRegistry().runDataStore());
- this.cloud = controller.serviceRegistry().testerCloud();
- this.metric = new JobMetrics(controller.metric());
- this.disabledZones = PermanentFlags.DISABLED_DEPLOYMENT_ZONES.bindTo(controller.flagSource());
- }
-
- public TesterCloud cloud() { return cloud; }
- public int historyLength() { return historyLength; }
- public void setRunner(Consumer<Run> runner) { this.runner.set(runner); }
-
- /** Rewrite all job data with the newest format. */
- public void updateStorage() {
- for (ApplicationId id : instances())
- for (JobType type : jobs(id)) {
- locked(id, type, runs -> { // Runs are not modified here, and are written as they were.
- curator.readLastRun(id, type).ifPresent(curator::writeLastRun);
- });
- }
- }
-
- public boolean isDisabled(JobId id) {
- return disabledZones.with(Dimension.INSTANCE_ID, id.application().serializedForm()).value().contains(id.type().zone().value());
- }
-
- /** Returns all entries currently logged for the given run. */
- public Optional<RunLog> details(RunId id) {
- return details(id, -1);
- }
-
- /** Returns the logged entries for the given run, which are after the given id threshold. */
- public Optional<RunLog> details(RunId id, long after) {
- try (Mutex __ = curator.lock(id.application(), id.type())) {
- Run run = runs(id.application(), id.type()).get(id);
- if (run == null)
- return Optional.empty();
-
- return active(id).isPresent()
- ? Optional.of(logs.readActive(id.application(), id.type(), after))
- : logs.readFinished(id, after);
- }
- }
-
- /** Stores the given log entries for the given run and step. */
- public void log(RunId id, Step step, List<LogEntry> entries) {
- locked(id, __ -> {
- logs.append(id.application(), id.type(), step, entries, true);
- return __;
- });
- }
-
- /** Stores the given log messages for the given run and step. */
- public void log(RunId id, Step step, Level level, List<String> messages) {
- log(id, step, messages.stream()
- .map(message -> new LogEntry(0, controller.clock().instant(), LogEntry.typeOf(level), message))
- .toList());
- }
-
- /** Stores the given log message for the given run and step. */
- public void log(RunId id, Step step, Level level, String message) {
- log(id, step, level, Collections.singletonList(message));
- }
-
- /** Fetches any new Vespa log entries, and records the timestamp of the last of these, for continuation. */
- public void updateVespaLog(RunId id) {
- locked(id, run -> {
- if ( ! run.hasStep(copyVespaLogs))
- return run;
-
- storeVespaLogs(id);
-
- // TODO jonmv: remove all the below around start of 2023.
- ZoneId zone = id.type().zone();
- Optional<Deployment> deployment = Optional.ofNullable(controller.applications().requireInstance(id.application())
- .deployments().get(zone));
- if (deployment.isEmpty() || deployment.get().at().isBefore(run.start()))
- return run;
-
- List<LogEntry> log;
- Optional<Instant> deployedAt;
- Instant from;
- if ( ! run.id().type().isProduction()) {
- deployedAt = run.stepInfo(installInitialReal).or(() -> run.stepInfo(installReal)).flatMap(StepInfo::startTime);
- if (deployedAt.isPresent()) {
- from = run.lastVespaLogTimestamp().isAfter(run.start()) ? run.lastVespaLogTimestamp() : deployedAt.get().minusSeconds(10);
- log = LogEntry.parseVespaLog(controller.serviceRegistry().configServer()
- .getLogs(new DeploymentId(id.application(), zone),
- Map.of("from", Long.toString(from.toEpochMilli()))),
- from);
- }
- else log = List.of();
- }
- else log = List.of();
-
- if (id.type().isTest()) {
- deployedAt = run.stepInfo(installTester).flatMap(StepInfo::startTime);
- if (deployedAt.isPresent()) {
- from = run.lastVespaLogTimestamp().isAfter(run.start()) ? run.lastVespaLogTimestamp() : deployedAt.get().minusSeconds(10);
- List<LogEntry> testerLog = LogEntry.parseVespaLog(controller.serviceRegistry().configServer()
- .getLogs(new DeploymentId(id.tester().id(), zone),
- Map.of("from", Long.toString(from.toEpochMilli()))),
- from);
-
- Instant justNow = controller.clock().instant().minusSeconds(2);
- log = Stream.concat(log.stream(), testerLog.stream())
- .filter(entry -> entry.at().isBefore(justNow))
- .sorted(comparing(LogEntry::at))
- .toList();
- }
- }
- if (log.isEmpty())
- return run;
-
- logs.append(id.application(), id.type(), Step.copyVespaLogs, log, false);
- return run.with(log.get(log.size() - 1).at());
- });
- }
-
- public InputStream getVespaLogs(RunId id, long fromMillis, boolean tester) {
- Run run = run(id);
- return run.stepStatus(copyVespaLogs).map(succeeded::equals).orElse(false)
- ? controller.serviceRegistry().runDataStore().getLogs(id, tester)
- : getVespaLogsFromLogserver(run, fromMillis, tester).orElse(InputStream.nullInputStream());
- }
-
- public static Optional<Instant> deploymentCompletedAt(Run run, boolean tester) {
- return (tester ? run.stepInfo(installTester)
- : run.stepInfo(installInitialReal).or(() -> run.stepInfo(installReal)))
- .flatMap(StepInfo::startTime).map(start -> start.minusSeconds(10));
- }
-
- public void storeVespaLogs(RunId id) {
- Run run = run(id);
- if ( ! id.type().isProduction()) {
- getVespaLogsFromLogserver(run, 0, false).ifPresent(logs -> {
- try (logs) {
- controller.serviceRegistry().runDataStore().putLogs(id, false, logs);
- }
- catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- });
- }
- if (id.type().isTest()) {
- getVespaLogsFromLogserver(run, 0, true).ifPresent(logs -> {
- try (logs) {
- controller.serviceRegistry().runDataStore().putLogs(id, true, logs);
- }
- catch(IOException e){
- throw new UncheckedIOException(e);
- }
- });
- }
- }
-
- private Optional<InputStream> getVespaLogsFromLogserver(Run run, long fromMillis, boolean tester) {
- return deploymentCompletedAt(run, tester).map(at ->
- controller.serviceRegistry().configServer().getLogs(new DeploymentId(tester ? run.id().tester().id() : run.id().application(),
- run.id().type().zone()),
- Map.of("from", Long.toString(Math.max(fromMillis, at.toEpochMilli())),
- "to", Long.toString(run.end().orElse(controller.clock().instant()).toEpochMilli()))));
-}
-
- /** Fetches any new test log entries, and records the id of the last of these, for continuation. */
- public void updateTestLog(RunId id) {
- locked(id, run -> {
- Optional<Step> step = Stream.of(endStagingSetup, endTests)
- .filter(run.readySteps()::contains)
- .findAny();
- if (step.isEmpty())
- return run;
-
- List<LogEntry> entries = cloud.getLog(new DeploymentId(id.tester().id(), id.type().zone()),
- run.lastTestLogEntry());
- if (entries.isEmpty())
- return run;
-
- logs.append(id.application(), id.type(), step.get(), entries, false);
- return run.with(entries.stream().mapToLong(LogEntry::id).max().getAsLong());
- });
- }
-
- public void updateTestReport(RunId id) {
- locked(id, run -> {
- Optional<TestReport> report = cloud.getTestReport(new DeploymentId(id.tester().id(), id.type().zone()));
- if (report.isEmpty()) {
- return run;
- }
- logs.writeTestReport(id, report.get());
- return run;
- });
- }
-
- public Optional<String> getTestReports(RunId id) {
- return logs.readTestReports(id);
- }
-
- /** Stores the given certificate as the tester certificate for this run, or throws if it's already set. */
- public void storeTesterCertificate(RunId id, X509Certificate testerCertificate) {
- locked(id, run -> run.with(testerCertificate));
- }
-
- /** Returns a list of all instances of applications which have registered. */
- public List<ApplicationId> instances() {
- return controller.applications().readable().stream()
- .flatMap(application -> application.instances().values().stream())
- .map(Instance::id).toList();
- }
-
- /** Returns all job types which have been run for the given application. */
- private List<JobType> jobs(ApplicationId id) {
- return JobType.allIn(controller.zoneRegistry()).stream()
- .filter(type -> last(id, type).isPresent()).toList();
- }
-
- /** Returns an immutable map of all known runs for the given application and job type. */
- public NavigableMap<RunId, Run> runs(JobId id) {
- return runs(id.application(), id.type());
- }
-
- /** Lists the start time of non-redeployment runs of the given job, in order of increasing age. */
- public List<Instant> jobStarts(JobId id) {
- return runs(id).descendingMap().values().stream()
- .filter(run -> !run.isRedeployment())
- .map(Run::start).toList();
- }
-
- /** Returns when given deployment last started deploying, falling back to time of deployment if it cannot be determined from job runs */
- public Instant lastDeploymentStart(ApplicationId instanceId, Deployment deployment) {
- return jobStarts(new JobId(instanceId, JobType.deploymentTo(deployment.zone()))).stream()
- .findFirst()
- .orElseGet(deployment::at);
- }
-
- /** Returns an immutable map of all known runs for the given application and job type. */
- public NavigableMap<RunId, Run> runs(ApplicationId id, JobType type) {
- ImmutableSortedMap.Builder<RunId, Run> runs = ImmutableSortedMap.orderedBy(Comparator.comparing(RunId::number));
- Optional<Run> last = last(id, type);
- curator.readHistoricRuns(id, type).forEach((runId, run) -> {
- if (last.isEmpty() || ! runId.equals(last.get().id()))
- runs.put(runId, run);
- });
- last.ifPresent(run -> runs.put(run.id(), run));
- return runs.build();
- }
-
- /** Returns the run with the given id, or throws if no such run exists. */
- public Run run(RunId id) {
- return runs(id.application(), id.type()).values().stream()
- .filter(run -> run.id().equals(id))
- .findAny()
- .orElseThrow(() -> new NoSuchElementException("no run with id '" + id + "' exists"));
- }
-
- /** Returns the last run of the given type, for the given application, if one has been run. */
- public Optional<Run> last(JobId job) {
- return curator.readLastRun(job.application(), job.type());
- }
-
- /** Returns the last run of the given type, for the given application, if one has been run. */
- public Optional<Run> last(ApplicationId id, JobType type) {
- return curator.readLastRun(id, type);
- }
-
- /** Returns the last completed of the given job. */
- public Optional<Run> lastCompleted(JobId id) {
- return JobStatus.lastCompleted(runs(id));
- }
-
- /** Returns the first failing of the given job. */
- public Optional<Run> firstFailing(JobId id) {
- return JobStatus.firstFailing(runs(id));
- }
-
- /** Returns the last success of the given job. */
- public Optional<Run> lastSuccess(JobId id) {
- return JobStatus.lastSuccess(runs(id));
- }
-
- /** Returns the run with the given id, provided it is still active. */
- public Optional<Run> active(RunId id) {
- return last(id.application(), id.type())
- .filter(run -> ! run.hasEnded())
- .filter(run -> run.id().equals(id));
- }
-
- /** Returns a list of all active runs. */
- public List<Run> active() {
- return controller.applications().idList().stream()
- .flatMap(id -> active(id).stream())
- .toList();
- }
-
- /** Returns a list of all active runs for the given application. */
- public List<Run> active(TenantAndApplicationId id) {
- return controller.applications().requireApplication(id).instances().keySet().stream()
- .flatMap(name -> JobType.allIn(controller.zoneRegistry()).stream()
- .map(type -> last(id.instance(name), type))
- .flatMap(Optional::stream)
- .filter(run -> ! run.hasEnded()))
- .toList();
- }
-
- /** Returns a list of all active runs for the given instance. */
- public List<Run> active(ApplicationId id) {
- return JobType.allIn(controller.zoneRegistry()).stream()
- .map(type -> last(id, type))
- .flatMap(Optional::stream)
- .filter(run -> !run.hasEnded())
- .toList();
- }
-
- /** Returns the job status of the given job, possibly empty. */
- public JobStatus jobStatus(JobId id) {
- return new JobStatus(id, runs(id));
- }
-
- /** Returns the deployment status of the given application. */
- public DeploymentStatus deploymentStatus(Application application) {
- VersionStatus versionStatus = controller.readVersionStatus();
- return deploymentStatus(application, versionStatus, controller.systemVersion(versionStatus));
- }
-
- private DeploymentStatus deploymentStatus(Application application, VersionStatus versionStatus, Version systemVersion) {
- return new DeploymentStatus(application,
- this::jobStatus,
- controller.zoneRegistry(),
- versionStatus,
- systemVersion,
- instance -> controller.applications().versionCompatibility(application.id().instance(instance)),
- controller.clock().instant());
- }
-
- /** Adds deployment status to each of the given applications. */
- public DeploymentStatusList deploymentStatuses(ApplicationList applications, VersionStatus versionStatus) {
- Version systemVersion = controller.systemVersion(versionStatus);
- return DeploymentStatusList.from(applications.asList().stream()
- .map(application -> deploymentStatus(application, versionStatus, systemVersion))
- .toList());
- }
-
- /** Adds deployment status to each of the given applications. Calling this will do an implicit read of the controller's version status */
- public DeploymentStatusList deploymentStatuses(ApplicationList applications) {
- VersionStatus versionStatus = controller.readVersionStatus();
- return deploymentStatuses(applications, versionStatus);
- }
-
- /** Changes the status of the given step, for the given run, provided it is still active. */
- public void update(RunId id, RunStatus status, LockedStep step) {
- locked(id, run -> run.with(status, step));
- }
-
- /**
- * Changes the status of the given run to inactive, and stores it as a historic run.
- * Throws TimeoutException if some step in this job is still being run.
- */
- public void finish(RunId id) throws TimeoutException {
- Deque<Mutex> locks = new ArrayDeque<>();
- try {
- // Ensure no step is still running before we finish the run — report depends transitively on all the other steps.
- Run unlockedRun = run(id);
- locks.push(curator.lock(id.application(), id.type(), report));
- for (Step step : report.allPrerequisites(unlockedRun.steps().keySet()))
- locks.push(curator.lock(id.application(), id.type(), step));
-
- locked(id, run -> {
- // If run should be reset, just return here.
- if (run.status() == reset) {
- for (Step step : run.steps().keySet())
- log(id, step, INFO, List.of("### Run will reset, and start over at " + run.sleepUntil().orElse(controller.clock().instant()).truncatedTo(SECONDS), ""));
- return run.reset();
- }
- if (run.status() == running && run.stepStatuses().values().stream().anyMatch(not(succeeded::equals))) return run;
-
- // Store the modified run after it has been written to history, in case the latter fails.
- Run finishedRun = run.finished(controller.clock().instant());
- locked(id.application(), id.type(), runs -> {
- runs.put(run.id(), finishedRun);
- long last = id.number();
- long successes = runs.values().stream().filter(Run::hasSucceeded).count();
- var oldEntries = runs.entrySet().iterator();
- for (var old = oldEntries.next();
- old.getKey().number() <= last - historyLength
- || old.getValue().start().isBefore(controller.clock().instant().minus(maxHistoryAge));
- old = oldEntries.next()) {
-
- // Make sure we keep the last success and the first failing
- if ( successes == 1
- && old.getValue().hasSucceeded()
- && ! old.getValue().start().isBefore(controller.clock().instant().minus(maxHistoryAge))) {
- oldEntries.next();
- continue;
- }
-
- logs.delete(old.getKey());
- oldEntries.remove();
- }
- });
- logs.flush(id);
- metric.jobFinished(run.id().job(), finishedRun.status());
- pruneRevisions(unlockedRun);
-
- return finishedRun;
- });
- }
- finally {
- for (Mutex lock : locks) {
- try {
- lock.close();
- } catch (Throwable t) {
- log.log(WARNING, "Failed to close the lock " + lock + ": the lock may or may not " +
- "have been released in ZooKeeper, and if not this controller " +
- "must be restarted to release the lock", t);
- }
- }
- }
- }
-
- /** Marks the given run as aborted; no further normal steps will run, but run-always steps will try to succeed. */
- public void abort(RunId id, String reason, boolean cancelledByHumans) {
- locked(id, run -> {
- if (run.status() == aborted || run.status() == cancelled)
- return run;
-
- run.stepStatuses().entrySet().stream()
- .filter(entry -> entry.getValue() == unfinished)
- .forEach(entry -> log(id, entry.getKey(), INFO, "Aborting run: " + reason));
- return run.aborted(cancelledByHumans);
- });
- }
-
- /** Accepts and stores a new application package and test jar pair under a generated application version key. */
- public ApplicationVersion submit(TenantAndApplicationId id, Submission submission, long projectId) {
- ApplicationController applications = controller.applications();
- AtomicReference<ApplicationVersion> version = new AtomicReference<>();
- applications.lockApplicationOrThrow(id, application -> {
- Optional<ApplicationVersion> previousVersion = application.get().revisions().last();
- Optional<ApplicationPackage> previousPackage = previousVersion.flatMap(previous -> applications.applicationStore().find(id.tenant(), id.application(), previous.buildNumber()))
- .map(ApplicationPackage::new);
- long previousBuild = previousVersion.map(latestVersion -> latestVersion.buildNumber()).orElse(0L);
- version.set(submission.toApplicationVersion(1 + previousBuild));
-
- byte[] diff = previousPackage.map(previous -> ApplicationPackageDiff.diff(previous, submission.applicationPackage()))
- .orElseGet(() -> ApplicationPackageDiff.diffAgainstEmpty(submission.applicationPackage()));
- applications.applicationStore().put(id.tenant(),
- id.application(),
- version.get().id(),
- submission.applicationPackage().zippedContent(),
- withDeploymentSpec(submission.testPackage(),
- submission.applicationPackage().deploymentSpec()),
- diff);
- applications.applicationStore().putMeta(id.tenant(),
- id.application(),
- controller.clock().instant(),
- submission.applicationPackage().metaDataZip());
-
- application = application.withProjectId(projectId == -1 ? OptionalLong.empty() : OptionalLong.of(projectId));
- application = application.withRevisions(revisions -> revisions.with(version.get()));
- application = withPrunedPackages(application, version.get().id());
- version.set(application.get().revisions().get(version.get().id()));
-
- validate(id, submission);
-
- List<InstanceName> newInstances = applications.storeWithUpdatedConfig(application, submission.applicationPackage());
- if (application.get().projectId().isPresent())
- applications.deploymentTrigger().triggerNewRevision(id);
- for (InstanceName instance : newInstances)
- controller.applications().deploymentTrigger().forceChange(id.instance(instance), Change.of(version.get().id()));
- });
- return version.get();
- }
-
- static byte[] withDeploymentSpec(byte[] testZip, DeploymentSpec spec) {
- ZipBuilder zip = new ZipBuilder(testZip.length + (1 << 12));
- try (zip) {
- zip.add(testZip, name -> !name.equals(deploymentFile));
- zip.add(deploymentFile, spec.xmlForm().getBytes(UTF_8));
- }
- return zip.toByteArray();
- }
-
- private void validate(TenantAndApplicationId id, Submission submission) {
- controller.notificationsDb().removeNotification(NotificationSource.from(id), Type.testPackage);
- controller.notificationsDb().removeNotification(NotificationSource.from(id), Type.submission);
-
- validateTests(id, submission);
- validateMajorVersion(id, submission);
- }
-
- private void validateTests(TenantAndApplicationId id, Submission submission) {
- var testSummary = TestPackage.validateTests(submission.applicationPackage().deploymentSpec(), submission.testPackage());
- if ( ! testSummary.problems().isEmpty())
- controller.notificationsDb().setTestPackageNotification(id, testSummary.problems());
- }
-
- private void validateMajorVersion(TenantAndApplicationId id, Submission submission) {
- submission.applicationPackage().deploymentSpec().majorVersion().ifPresent(explicitMajor -> {
- if ( ! controller.readVersionStatus().isOnCurrentMajor(new Version(explicitMajor)))
- controller.notificationsDb().setSubmissionNotification(id,
- "Vespa " + explicitMajor + " will soon reach end of life, upgrade to [Vespa " + (explicitMajor + 1) + " now](" +
- "https://cloud.vespa.ai/en/vespa" + (explicitMajor + 1) + "-release-notes.html)"); // ∠( ᐛ 」∠)_
- });
- }
-
- private LockedApplication withPrunedPackages(LockedApplication application, RevisionId latest) {
- TenantAndApplicationId id = application.get().id();
- Application wrapped = application.get();
- RevisionId oldestDeployed = application.get().oldestDeployedRevision()
- .or(() -> wrapped.instances().values().stream()
- .flatMap(instance -> instance.change().revision().stream())
- .min(naturalOrder()))
- .orElse(latest);
- RevisionId oldestToKeep = null;
- Instant now = controller.clock().instant();
- for (ApplicationVersion version : application.get().revisions().withPackage()) {
- if (version.id().compareTo(oldestDeployed) < 0) {
- if (version.obsoleteAt().isEmpty()) {
- application = application.withRevisions(revisions -> revisions.with(version.obsoleteAt(now)));
- if (oldestToKeep == null)
- oldestToKeep = version.id();
- }
- else {
- if (oldestToKeep == null && !version.obsoleteAt().get().isBefore(now.minus(obsoletePackageExpiry)))
- oldestToKeep = version.id();
- }
- }
- }
-
- if (oldestToKeep != null) {
- controller.applications().applicationStore().prune(id.tenant(), id.application(), oldestToKeep);
- for (ApplicationVersion version : application.get().revisions().withPackage())
- if (version.id().compareTo(oldestToKeep) < 0)
- application = application.withRevisions(revisions -> revisions.with(version.withoutPackage()));
- }
- return application;
- }
-
- /** Forget revisions no longer present in any relevant job history. */
- private void pruneRevisions(Run run) {
- TenantAndApplicationId applicationId = TenantAndApplicationId.from(run.id().application());
- boolean isProduction = run.versions().targetRevision().isProduction();
- (isProduction ? deploymentStatus(controller.applications().requireApplication(applicationId)).jobs().asList().stream()
- : Stream.of(jobStatus(run.id().job())))
- .flatMap(jobs -> jobs.runs().values().stream())
- .map(r -> r.versions().targetRevision())
- .filter(id -> id.isProduction() == isProduction)
- .min(naturalOrder())
- .ifPresent(oldestRevision -> {
- controller.applications().lockApplicationOrThrow(applicationId, application -> {
- if (isProduction) {
- controller.applications().applicationStore().pruneDiffs(run.id().application().tenant(), run.id().application().application(), oldestRevision.number());
- controller.applications().store(application.withRevisions(revisions -> revisions.withoutOlderThan(oldestRevision)));
- }
- else {
- controller.applications().applicationStore().pruneDevDiffs(new DeploymentId(run.id().application(), run.id().job().type().zone()), oldestRevision.number());
- controller.applications().store(application.withRevisions(revisions -> revisions.withoutOlderThan(oldestRevision, run.id().job())));
- }
- });
- });
- }
-
- /** Orders a run of the given type, or throws an IllegalStateException if that job type is already running. */
- public void start(ApplicationId id, JobType type, Versions versions, boolean isRedeployment, Reason reason) {
- start(id, type, versions, isRedeployment, JobProfile.of(type), reason);
- }
-
- /** Orders a run of the given type, or throws an IllegalStateException if that job type is already running. */
- public void start(ApplicationId id, JobType type, Versions versions, boolean isRedeployment, JobProfile profile, Reason reason) {
- ApplicationVersion revision = controller.applications().requireApplication(TenantAndApplicationId.from(id)).revisions().get(versions.targetRevision());
- if (revision.compileVersion()
- .map(version -> controller.applications().versionCompatibility(id).refuse(versions.targetPlatform(), version))
- .orElse(false))
- throw new IllegalArgumentException("Will not start " + type + " for " + id + " with incompatible platform version (" +
- versions.targetPlatform() + ") " + "and compile versions (" + revision.compileVersion().get() + ")");
-
- locked(id, type, __ -> {
- Optional<Run> last = last(id, type);
- if (last.flatMap(run -> active(run.id())).isPresent())
- throw new IllegalArgumentException("Cannot start " + type + " for " + id + "; it is already running!");
-
- RunId newId = new RunId(id, type, last.map(run -> run.id().number()).orElse(0L) + 1);
- curator.writeLastRun(Run.initial(newId, versions, isRedeployment, controller.clock().instant(), profile, reason));
- metric.jobStarted(newId.job());
- });
- }
-
-
- /** Stores the given package and starts a deployment of it, after aborting any such ongoing deployment. */
- public void deploy(ApplicationId id, JobType type, Optional<Version> platform, ApplicationPackage applicationPackage) {
- deploy(id, type, platform, applicationPackage, false, false);
- }
-
- /** Stores the given package and starts a deployment of it, after aborting any such ongoing deployment.*/
- public void deploy(ApplicationId id, JobType type, Optional<Version> platform, ApplicationPackage applicationPackage,
- boolean dryRun, boolean allowOutdatedPlatform) {
- if ( ! controller.zoneRegistry().hasZone(type.zone()))
- throw new IllegalArgumentException(type.zone() + " is not present in this system");
-
- VersionStatus versionStatus = controller.readVersionStatus();
- if ( ! controller.system().isCd()
- && platform.isPresent()
- && versionStatus.deployableVersions().stream().map(VespaVersion::versionNumber).noneMatch(platform.get()::equals))
- throw new IllegalArgumentException("platform version " + platform.get() + " is not present in this system");
-
- controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- if ( ! application.get().instances().containsKey(id.instance()))
- application = controller.applications().withNewInstance(application, id);
- // TODO(mpolden): Enable for public CD once all tests have been updated
- if (controller.system() != SystemName.PublicCd) {
- controller.applications().validatePackage(applicationPackage, application.get());
- controller.applications().decideCloudAccountOf(new DeploymentId(id, type.zone()), applicationPackage.deploymentSpec());
- }
- controller.applications().store(application);
- });
-
- DeploymentId deploymentId = new DeploymentId(id, type.zone());
- Optional<Run> lastRun = last(id, type);
- lastRun.filter(run -> ! run.hasEnded()).ifPresent(run -> abortAndWait(run.id(), Duration.ofMinutes(2)));
-
- long build = 1 + lastRun.map(run -> run.versions().targetRevision().number()).orElse(0L);
- RevisionId revisionId = RevisionId.forDevelopment(build, new JobId(id, type));
- ApplicationVersion version = ApplicationVersion.forDevelopment(revisionId, applicationPackage.compileVersion(), applicationPackage.deploymentSpec().majorVersion());
-
- byte[] diff = getDiff(applicationPackage, deploymentId, lastRun);
-
- controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- Version targetPlatform = platform.orElseGet(() -> findTargetPlatform(applicationPackage, deploymentId, application.get().get(id.instance()), versionStatus));
- if ( ! allowOutdatedPlatform
- && ! controller.readVersionStatus().isOnCurrentMajor(targetPlatform)
- && runs(id, type).values().stream().noneMatch(run -> run.versions().targetPlatform().getMajor() == targetPlatform.getMajor()))
- throw new IllegalArgumentException("platform version " + targetPlatform + " is not on a current major version in this system");
-
- controller.applications().applicationStore().putDev(deploymentId, version.id(), applicationPackage.zippedContent(), diff);
- controller.applications().store(application.withRevisions(revisions -> revisions.with(version)));
- Optional<Deployment> existing = application.get().get(id.instance()).map(instance -> instance.deployments().get(type.zone()));
- start(id,
- type,
- new Versions(targetPlatform, version.id(), existing.map(Deployment::version), existing.map(Deployment::revision)),
- false,
- dryRun ? JobProfile.developmentDryRun : JobProfile.development,
- Reason.empty());
- });
-
- locked(id, type, __ -> {
- runner.get().accept(last(id, type).get());
- });
- }
-
- /* Application package diff against previous version, or against empty version if previous does not exist or is invalid */
- private byte[] getDiff(ApplicationPackage applicationPackage, DeploymentId deploymentId, Optional<Run> lastRun) {
- return lastRun.map(run -> run.versions().targetRevision())
- .map(prevVersion -> {
- ApplicationPackage previous;
- try {
- previous = new ApplicationPackage(controller.applications().applicationStore().get(deploymentId, prevVersion));
- } catch (RuntimeException e) {
- return ApplicationPackageDiff.diffAgainstEmpty(applicationPackage);
- }
- return ApplicationPackageDiff.diff(previous, applicationPackage);
- })
- .orElseGet(() -> ApplicationPackageDiff.diffAgainstEmpty(applicationPackage));
- }
-
- private Version findTargetPlatform(ApplicationPackage applicationPackage, DeploymentId id, Optional<Instance> instance, VersionStatus versionStatus) {
- // Prefer previous platform if possible. Candidates are all deployable, ascending, with existing version appended; then reversed.
- Version systemVersion = controller.systemVersion(versionStatus);
-
- List<Version> versions = new ArrayList<>(List.of(systemVersion));
- for (VespaVersion version : versionStatus.deployableVersions())
- if (version.confidence().equalOrHigherThan(Confidence.normal))
- versions.add(version.versionNumber());
-
- instance.map(Instance::deployments)
- .map(deployments -> deployments.get(id.zoneId()))
- .map(Deployment::version)
- .filter(versions::contains) // Don't deploy versions that are no longer known.
- .ifPresent(versions::add);
-
- // Remove all versions that are older than the compile version.
- versions.removeIf(version -> applicationPackage.compileVersion().map(version::isBefore).orElse(false));
- if (versions.isEmpty()) {
- // Fall back to the newest deployable version, if all the ones with normal confidence were too old.
- Iterator<VespaVersion> descending = reversed(versionStatus.deployableVersions()).iterator();
- if ( ! descending.hasNext())
- throw new IllegalStateException("no deployable platform version found in the system");
- else
- versions.add(descending.next().versionNumber());
- }
-
- VersionCompatibility compatibility = controller.applications().versionCompatibility(id.applicationId());
- List<Version> compatibleVersions = new ArrayList<>();
- for (Version target : reversed(versions))
- if (applicationPackage.compileVersion().isEmpty() || compatibility.accept(target, applicationPackage.compileVersion().get()))
- compatibleVersions.add(target);
-
- if (compatibleVersions.isEmpty())
- throw new IllegalArgumentException("no platforms are compatible with compile version " + applicationPackage.compileVersion().get());
-
- Optional<Integer> major = applicationPackage.deploymentSpec().majorVersion();
- List<Version> versionOnRightMajor = new ArrayList<>();
- for (Version target : reversed(versions))
- if (major.isEmpty() || major.get() == target.getMajor())
- versionOnRightMajor.add(target);
-
- if (versionOnRightMajor.isEmpty())
- throw new IllegalArgumentException("no platforms were found for major version " + major.get() + " specified in deployment.xml");
-
- for (Version target : compatibleVersions)
- if (versionOnRightMajor.contains(target))
- return target;
-
- throw new IllegalArgumentException("no platforms on major version " + major.get() + " specified in deployment.xml " +
- "are compatible with compile version " + applicationPackage.compileVersion().get());
- }
-
- /** Aborts a run and waits for it complete. */
- private void abortAndWait(RunId id, Duration timeout) {
- abort(id, "replaced by new deployment", true);
- runner.get().accept(last(id.application(), id.type()).get());
-
- Instant doom = controller.clock().instant().plus(timeout);
- Duration sleep = Duration.ofMillis(100);
- while ( ! last(id.application(), id.type()).get().hasEnded()) {
- if (controller.clock().instant().plus(sleep).isAfter(doom))
- throw new UncheckedTimeoutException("timeout waiting for " + id + " to abort and finish");
- try {
- Thread.sleep(sleep.toMillis());
- }
- catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new RuntimeException(e);
- }
- }
- }
-
- /** Deletes run data and tester deployments for applications which are unknown, or no longer built internally. */
- public void collectGarbage() {
- Set<ApplicationId> applicationsToBuild = new HashSet<>(instances());
- curator.applicationsWithJobs().stream()
- .filter(id -> ! applicationsToBuild.contains(id))
- .forEach(id -> {
- try {
- TesterId tester = TesterId.of(id);
- for (JobType type : jobs(id))
- locked(id, type, deactivateTester, __ -> {
- try (Mutex ___ = curator.lock(id, type)) {
- try {
- deactivateTester(tester, type);
- }
- catch (Exception e) {
- // It's probably already deleted, so if we fail, that's OK.
- }
- curator.deleteRunData(id, type);
- }
- });
- logs.delete(id);
- curator.deleteRunData(id);
- }
- catch (Exception e) {
- log.log(WARNING, "failed cleaning up after deleted application", e);
- }
- });
- }
-
- public void deactivateTester(TesterId id, JobType type) {
- controller.serviceRegistry().configServer().deactivate(new DeploymentId(id.id(), type.zone()));
- }
-
- /** Locks all runs and modifies the list of historic runs for the given application and job type. */
- private void locked(ApplicationId id, JobType type, Consumer<SortedMap<RunId, Run>> modifications) {
- try (Mutex __ = curator.lock(id, type)) {
- SortedMap<RunId, Run> runs = new TreeMap<>(curator.readHistoricRuns(id, type));
- modifications.accept(runs);
- curator.writeHistoricRuns(id, type, runs.values());
- }
- }
-
- /** Locks and modifies the run with the given id, provided it is still active. */
- public void locked(RunId id, UnaryOperator<Run> modifications) {
- try (Mutex __ = curator.lock(id.application(), id.type())) {
- active(id).ifPresent(run -> {
- Run modified = modifications.apply(run);
- if (modified != null) curator.writeLastRun(modified);
- });
- }
- }
-
- /** Locks the given step and checks none of its prerequisites are running, then performs the given actions. */
- public void locked(ApplicationId id, JobType type, Step step, Consumer<LockedStep> action) throws TimeoutException {
- try (Mutex lock = curator.lock(id, type, step)) {
- for (Step prerequisite : step.allPrerequisites(last(id, type).get().steps().keySet())) // Check that no prerequisite is still running.
- try (Mutex __ = curator.lock(id, type, prerequisite)) { ; }
-
- action.accept(new LockedStep(lock, step));
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java
deleted file mode 100644
index 95ea3ff1ffb..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobList.java
+++ /dev/null
@@ -1,221 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.collections.AbstractFilteringList;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-
-import java.time.Instant;
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.aborted;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.cancelled;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.nodeAllocationFailure;
-
-/**
- * A list of deployment jobs that can be filtered in various ways.
- *
- * @author jonmv
- */
-public class JobList extends AbstractFilteringList<JobStatus, JobList> {
-
- private JobList(Collection<? extends JobStatus> jobs, boolean negate) {
- super(jobs, negate, JobList::new);
- }
-
- // ----------------------------------- Factories
-
- public static JobList from(Collection<? extends JobStatus> jobs) {
- return new JobList(jobs, false);
- }
-
- // ----------------------------------- Basic filters
-
- /** Returns the status of the job of the given type, if it is contained in this. */
- public Optional<JobStatus> get(JobId id) {
- return asList().stream().filter(job -> job.id().equals(id)).findAny();
- }
-
- /** Returns the subset of jobs which are currently upgrading */
- public JobList upgrading() {
- return matching(job -> job.isRunning()
- && job.lastSuccess().isPresent()
- && job.lastSuccess().get().versions().targetPlatform().isBefore(job.lastTriggered().get().versions().targetPlatform()));
- }
-
- /** Returns the subset of jobs which are currently failing */
- public JobList failing() {
- return matching(job -> job.lastCompleted().isPresent() && ! job.isSuccess());
- }
-
- /** Returns the subset of jobs which are currently failing, not out of test capacity, and not aborted. */
- public JobList failingHard() {
- return failing().not().outOfTestCapacity().not().withStatus(aborted).not().withStatus(cancelled);
- }
-
- public JobList outOfTestCapacity() {
- return matching(job -> job.isNodeAllocationFailure() && job.id().type().environment().isTest());
- }
-
- public JobList running() {
- return matching(job -> job.isRunning());
- }
-
- /** Returns the subset of jobs which must be failing due to an application change */
- public JobList failingApplicationChange() {
- return matching(JobList::failingApplicationChange);
- }
-
- /** Returns the subset of jobs which are failing because of an application change, and have been since the threshold, on the given revision. */
- public JobList failingWithBrokenRevisionSince(RevisionId broken, Instant threshold) {
- return failingApplicationChange().matching(job -> job.runs().values().stream()
- .anyMatch(run -> run.versions().targetRevision().equals(broken)
- && run.hasFailed()
- && run.start().isBefore(threshold)));
- }
-
- /** Returns the subset of jobs which are failing with the given run status. */
- public JobList withStatus(RunStatus status) {
- return matching(job -> job.lastStatus().map(status::equals).orElse(false));
- }
-
- /** Returns the subset of jobs of the given type -- most useful when negated. */
- public JobList type(Collection<? extends JobType> types) {
- return matching(job -> types.contains(job.id().type()));
- }
-
- /** Returns the subset of jobs of the given type -- most useful when negated. */
- public JobList type(JobType... types) {
- return type(List.of(types));
- }
-
- /** Returns the subset of jobs run for the given instance. */
- public JobList instance(InstanceName... instances) {
- return instance(Set.of(instances));
- }
-
- /** Returns the subset of jobs run for the given instance. */
- public JobList instance(Collection<InstanceName> instances) {
- return matching(job -> instances.contains(job.id().application().instance()));
- }
-
- /** Returns the subset of jobs of which are production jobs. */
- public JobList production() {
- return matching(job -> job.id().type().isProduction());
- }
-
- /** Returns the subset of jobs which are test jobs. */
- public JobList test() {
- return matching(job -> job.id().type().isTest());
- }
-
- /** Returns the jobs with any runs failing with non-out-of-test-capacity on the given versions — targets only for system test, everything present otherwise. */
- public JobList failingHardOn(Versions versions) {
- return matching(job -> ! RunList.from(job)
- .on(versions)
- .matching(Run::hasFailed)
- .not().matching(run -> run.status() == nodeAllocationFailure && run.id().type().environment().isTest())
- .isEmpty());
- }
-
- /** Returns the jobs with any runs matching the given versions — targets only for system test, everything present otherwise. */
- public JobList triggeredOn(Versions versions) {
- return matching(job -> ! RunList.from(job).on(versions).isEmpty());
- }
-
- /** Returns the jobs with successful runs matching the given versions — targets only for system test, everything present otherwise. */
- public JobList successOn(JobType type, Versions versions) {
- return matching(job -> job.id().type().equals(type)
- && ! RunList.from(job)
- .matching(run -> run.hasSucceeded() && run.id().type().zone().equals(type.zone()))
- .on(versions)
- .isEmpty());
- }
-
- // ----------------------------------- JobRun filtering
-
- /** Returns the list in a state where the next filter is for the lastTriggered run type */
- public RunFilter lastTriggered() {
- return new RunFilter(JobStatus::lastTriggered);
- }
-
- /** Returns the list in a state where the next filter is for the lastCompleted run type */
- public RunFilter lastCompleted() {
- return new RunFilter(JobStatus::lastCompleted);
- }
-
- /** Returns the list in a state where the next filter is for the lastSuccess run type */
- public RunFilter lastSuccess() {
- return new RunFilter(JobStatus::lastSuccess);
- }
-
- /** Returns the list in a state where the next filter is for the firstFailing run type */
- public RunFilter firstFailing() {
- return new RunFilter(JobStatus::firstFailing);
- }
-
- /** Allows sub-filters for runs of the indicated kind */
- public class RunFilter {
-
- private final Function<JobStatus, Optional<Run>> which;
-
- private RunFilter(Function<JobStatus, Optional<Run>> which) {
- this.which = which;
- }
-
- /** Returns the subset of jobs where the run of the indicated type exists */
- public JobList present() {
- return matching(run -> true);
- }
-
- /** Returns the runs of the indicated kind, mapped by the given function, as a list. */
- public <OtherType> List<OtherType> mapToList(Function<? super Run, OtherType> mapper) {
- return present().mapToList(which.andThen(Optional::get).andThen(mapper));
- }
-
- /** Returns the runs of the indicated kind. */
- public List<Run> asList() {
- return mapToList(Function.identity());
- }
-
- /** Returns the subset of jobs where the run of the indicated type ended no later than the given instant */
- public JobList endedNoLaterThan(Instant threshold) {
- return matching(run -> ! run.end().orElse(Instant.MAX).isAfter(threshold));
- }
-
- /** Returns the subset of jobs where the run of the indicated type was on the given version */
- public JobList on(RevisionId revision) {
- return matching(run -> run.versions().targetRevision().equals(revision));
- }
-
- /** Returns the subset of jobs where the run of the indicated type was on the given version */
- public JobList on(Version version) {
- return matching(run -> run.versions().targetPlatform().equals(version));
- }
-
- /** Transforms the JobRun condition to a JobStatus condition, by considering only the JobRun mapped by which, and executes */
- private JobList matching(Predicate<Run> condition) {
- return JobList.this.matching(job -> which.apply(job).filter(condition).isPresent());
- }
-
- }
-
- // ----------------------------------- Internal helpers
-
- private static boolean failingApplicationChange(JobStatus job) {
- if (job.isSuccess()) return false;
- if (job.lastSuccess().isEmpty()) return true; // An application which never succeeded is surely bad.
- if ( ! job.firstFailing().get().versions().targetPlatform().equals(job.lastSuccess().get().versions().targetPlatform())) return false; // Version change may be to blame.
- return ! job.firstFailing().get().versions().targetRevision().equals(job.lastSuccess().get().versions().targetRevision()); // Return whether there is an application change.
- }
-
-}
-
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java
deleted file mode 100644
index 6a0f5e44c9e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobMetrics.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import ai.vespa.metrics.ControllerMetrics;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-
-import java.util.Map;
-
-/**
- * Records metrics related to deployment jobs.
- *
- * @author jonmv
- */
-public class JobMetrics {
-
- public static final String start = ControllerMetrics.DEPLOYMENT_START.baseName();
- public static final String nodeAllocationFailure = ControllerMetrics.DEPLOYMENT_NODE_ALLOCATION_FAILURE.baseName();
- public static final String endpointCertificateTimeout = ControllerMetrics.DEPLOYMENT_ENDPOINT_CERTIFICATE_TIMEOUT.baseName();
- public static final String deploymentFailure = ControllerMetrics.DEPLOYMENT_DEPLOYMENT_FAILURE.baseName();
- public static final String invalidApplication = ControllerMetrics.DEPLOYMENT_INVALID_APPLICATION.baseName();
- public static final String convergenceFailure = ControllerMetrics.DEPLOYMENT_CONVERGENCE_FAILURE.baseName();
- public static final String testFailure = ControllerMetrics.DEPLOYMENT_TEST_FAILURE.baseName();
- public static final String noTests = ControllerMetrics.DEPLOYMENT_NO_TESTS.baseName();
- public static final String error = ControllerMetrics.DEPLOYMENT_ERROR.baseName();
- public static final String abort = ControllerMetrics.DEPLOYMENT_ABORT.baseName();
- public static final String cancel = ControllerMetrics.DEPLOYMENT_CANCEL.baseName();
- public static final String success = ControllerMetrics.DEPLOYMENT_SUCCESS.baseName();
- public static final String quotaExceeded = ControllerMetrics.DEPLOYMENT_QUOTA_EXCEEDED.baseName();
-
- private final Metric metric;
-
- public JobMetrics(Metric metric) {
- this.metric = metric;
- }
-
- public void jobStarted(JobId id) {
- metric.add(start, 1, metric.createContext(contextOf(id)));
- }
-
- public void jobFinished(JobId id, RunStatus status) {
- metric.add(valueOf(status), 1, metric.createContext(contextOf(id)));
- }
-
- Map<String, String> contextOf(JobId id) {
- return Map.of("applicationId", id.application().toFullString(),
- "tenantName", id.application().tenant().value(),
- "app", id.application().application().value() + "." + id.application().instance().value(),
- "test", Boolean.toString(id.type().isTest()),
- "zone", id.type().zone().value());
- }
-
- static String valueOf(RunStatus status) {
- return switch (status) {
- case nodeAllocationFailure -> nodeAllocationFailure;
- case endpointCertificateTimeout -> endpointCertificateTimeout;
- case invalidApplication -> invalidApplication;
- case deploymentFailed -> deploymentFailure;
- case installationFailed -> convergenceFailure;
- case testFailure -> testFailure;
- case noTests -> noTests;
- case error -> error;
- case cancelled -> cancel;
- case aborted -> abort;
- case success -> success;
- case quotaExceeded -> quotaExceeded;
- default -> throw new IllegalArgumentException("Unexpected run status '" + status + "'");
- };
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobProfile.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobProfile.java
deleted file mode 100644
index 1f8d2090471..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobProfile.java
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.Set;
-
-import static com.yahoo.vespa.hosted.controller.deployment.Step.copyVespaLogs;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployInitialReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.endStagingSetup;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.endTests;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installInitialReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.report;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.startStagingSetup;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.startTests;
-
-/**
- * Static profiles defining the {@link Step}s of a deployment job.
- *
- * @author jonmv
- */
-public enum JobProfile {
-
- systemTest(EnumSet.of(deployReal,
- installReal,
- deployTester,
- installTester,
- startTests,
- endTests,
- copyVespaLogs,
- deactivateTester,
- deactivateReal,
- report)),
-
- stagingTest(EnumSet.of(deployInitialReal,
- deployTester,
- installTester,
- installInitialReal,
- startStagingSetup,
- endStagingSetup,
- deployReal,
- installReal,
- startTests,
- endTests,
- copyVespaLogs,
- deactivateTester,
- deactivateReal,
- report)),
-
- production(EnumSet.of(deployReal,
- installReal,
- report)),
-
- productionTest(EnumSet.of(deployTester,
- installTester,
- startTests,
- endTests,
- copyVespaLogs,
- deactivateTester,
- report)),
-
- development(EnumSet.of(deployReal,
- installReal,
- copyVespaLogs)),
-
- developmentDryRun(EnumSet.of(deployReal));
-
-
- private final Set<Step> steps;
-
- JobProfile(Set<Step> steps) {
- this.steps = Collections.unmodifiableSet(steps);
- }
-
- // TODO jonmv: Let caller decide profile, and store with run?
- public static JobProfile of(JobType type) {
- switch (type.environment()) {
- case test: return systemTest;
- case staging: return stagingTest;
- case prod: return type.isTest() ? productionTest : production;
- case perf:
- case dev: return development;
- default: throw new AssertionError("Unexpected environment '" + type.environment() + "'!");
- }
- }
-
- /** Returns all steps in this profile, the default for which is to run only when all prerequisites are successes. */
- public Set<Step> steps() { return steps; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java
deleted file mode 100644
index 3770c9cd694..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/JobStatus.java
+++ /dev/null
@@ -1,107 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-
-import java.util.NavigableMap;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Aggregates information about all known runs of a given job to provide the high level status.
- *
- * @author jonmv
- */
-public class JobStatus {
-
- private final JobId id;
- private final NavigableMap<RunId, Run> runs;
- private final Optional<Run> lastTriggered;
- private final Optional<Run> lastCompleted;
- private final Optional<Run> lastSuccess;
- private final Optional<Run> firstFailing;
-
- public JobStatus(JobId id, NavigableMap<RunId, Run> runs) {
- this.id = Objects.requireNonNull(id);
- this.runs = Objects.requireNonNull(runs);
- this.lastTriggered = runs.descendingMap().values().stream().findFirst();
- this.lastCompleted = lastCompleted(runs);
- this.lastSuccess = lastSuccess(runs);
- this.firstFailing = firstFailing(runs);
- }
-
- public JobId id() {
- return id;
- }
-
- public NavigableMap<RunId, Run> runs() {
- return runs;
- }
-
- public Optional<Run> lastTriggered() {
- return lastTriggered;
- }
-
- public Optional<Run> lastCompleted() {
- return lastCompleted;
- }
-
- public Optional<Run> lastSuccess() {
- return lastSuccess;
- }
-
- public Optional<Run> firstFailing() {
- return firstFailing;
- }
-
- public Optional<RunStatus> lastStatus() {
- return lastCompleted().map(Run::status);
- }
-
- public boolean isSuccess() {
- return lastCompleted.map(last -> ! last.hasFailed()).orElse(false);
- }
-
- public boolean isRunning() {
- return lastTriggered.isPresent() && ! lastTriggered.get().hasEnded();
- }
-
- public boolean isNodeAllocationFailure() {
- return lastStatus().isPresent() && lastStatus().get() == RunStatus.nodeAllocationFailure;
- }
-
- @Override
- public String toString() {
- return "JobStatus{" +
- "id=" + id +
- ", lastTriggered=" + lastTriggered +
- ", lastCompleted=" + lastCompleted +
- ", lastSuccess=" + lastSuccess +
- ", firstFailing=" + firstFailing +
- '}';
- }
-
- static Optional<Run> lastCompleted(NavigableMap<RunId, Run> runs) {
- return runs.descendingMap().values().stream()
- .filter(run -> run.hasEnded())
- .findFirst();
- }
-
- static Optional<Run> lastSuccess(NavigableMap<RunId, Run> runs) {
- return runs.descendingMap().values().stream()
- .filter(Run::hasSucceeded)
- .findFirst();
- }
-
- static Optional<Run> firstFailing(NavigableMap<RunId, Run> runs) {
- Run failed = null;
- for (Run run : runs.descendingMap().values()) {
- if ( ! run.hasEnded()) continue;
- if ( ! run.hasFailed()) break;
- failed = run;
- }
- return Optional.ofNullable(failed);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/LockedStep.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/LockedStep.java
deleted file mode 100644
index 9f471116e22..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/LockedStep.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.curator.Lock;
-
-/**
- * @author jonmv
- */
-public class LockedStep {
-
- private final Step step;
- LockedStep(Mutex lock, Step step) { this.step = step; }
- public Step get() { return step; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeList.java
deleted file mode 100644
index a3aefa55f4e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeList.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.collections.AbstractFilteringList;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ServiceConvergence;
-
-import java.time.Instant;
-import java.util.Collection;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * @author jonmv
- */
-public class NodeList extends AbstractFilteringList<NodeWithServices, NodeList> {
-
- private final long wantedConfigGeneration;
-
- private NodeList(Collection<? extends NodeWithServices> items, boolean negate, long wantedConfigGeneration) {
- super(items, negate, (i, n) -> new NodeList(i, n, wantedConfigGeneration));
- this.wantedConfigGeneration = wantedConfigGeneration;
- }
-
- public static NodeList of(List<Node> nodes, List<Node> parents, ServiceConvergence services) {
- var servicesByHostName = services.services().stream()
- .collect(Collectors.groupingBy(service -> service.host()));
- var parentsByHostName = parents.stream()
- .collect(Collectors.toMap(node -> node.hostname(), node -> node));
- return new NodeList(nodes.stream()
- .map(node -> new NodeWithServices(node,
- parentsByHostName.get(node.parentHostname().get()),
- services.wantedGeneration(),
- servicesByHostName.getOrDefault(node.hostname(), List.of())))
- .toList(),
- false,
- services.wantedGeneration());
- }
-
- /** The nodes on an outdated OS. */
- public NodeList needsOsUpgrade() {
- return matching(NodeWithServices::needsOsUpgrade);
- }
-
- /** The nodes with outdated firmware. */
- public NodeList needsFirmwareUpgrade() {
- return matching(NodeWithServices::needsFirmwareUpgrade);
- }
-
- /** The nodes whose parent is down. */
- public NodeList withParentDown() {
- return matching(NodeWithServices::hasParentDown);
- }
-
- /** The nodes on an outdated platform. */
- public NodeList needsPlatformUpgrade() {
- return matching(NodeWithServices::needsPlatformUpgrade);
- }
-
- /** The nodes in need of a reboot. */
- public NodeList needsReboot() {
- return matching(NodeWithServices::needsReboot);
- }
-
- /** The nodes in need of a restart. */
- public NodeList needsRestart() {
- return matching(NodeWithServices::needsRestart);
- }
-
- /** The nodes currently allowed to be down. */
- public NodeList allowedDown() {
- return matching(node -> node.isAllowedDown());
- }
-
- /** The nodes currently expected to be down. */
- public NodeList expectedDown() {
- return matching(node -> node.isAllowedDown() || node.isNewlyProvisioned());
- }
-
- /** The nodes which have been suspended since before the given instant. */
- public NodeList suspendedSince(Instant instant) {
- return matching(node -> node.isSuspendedSince(instant));
- }
-
- /** The nodes with services on outdated config generation. */
- public NodeList needsNewConfig() {
- return matching(NodeWithServices::needsNewConfig);
- }
-
- public NodeList isStateful() {
- return matching(NodeWithServices::isStateful);
- }
-
- /** The nodes that are retiring. */
- public NodeList retiring() {
- return matching(node -> node.node().retired());
- }
-
-
- /** Returns a summary of the convergence status of the nodes in this list. */
- public ConvergenceSummary summary() {
- NodeList allowedDown = expectedDown();
- return new ConvergenceSummary(size(),
- allowedDown.size(),
- withParentDown().needsOsUpgrade().size(),
- withParentDown().needsFirmwareUpgrade().size(),
- needsPlatformUpgrade().size(),
- allowedDown.needsPlatformUpgrade().size(),
- needsReboot().size(),
- allowedDown.needsReboot().size(),
- needsRestart().size(),
- allowedDown.needsRestart().size(),
- asList().stream().mapToLong(node -> node.services().size()).sum(),
- asList().stream().mapToLong(node -> node.services().stream().filter(service -> wantedConfigGeneration > service.currentGeneration()).count()).sum(),
- retiring().size());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeWithServices.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeWithServices.java
deleted file mode 100644
index 39addbd3b63..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/NodeWithServices.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ServiceConvergence;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Objects;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Aggregate of a node and its services, fetched from different sources.
- *
- * @author jonmv
- */
-public class NodeWithServices {
-
- private final Node node;
- private final Node parent;
- private final long wantedConfigGeneration;
- private final List<ServiceConvergence.Status> services;
-
- public NodeWithServices(Node node, Node parent, long wantedConfigGeneration, List<ServiceConvergence.Status> services) {
- this.node = requireNonNull(node);
- this.parent = requireNonNull(parent);
- if (wantedConfigGeneration <= 0)
- throw new IllegalArgumentException("Wanted config generation must be positive");
- this.wantedConfigGeneration = wantedConfigGeneration;
- this.services = List.copyOf(services);
- }
-
- public Node node() { return node; }
- public Node parent() { return parent; }
- public long wantedConfigGeneration() { return wantedConfigGeneration; }
- public List<ServiceConvergence.Status> services() { return services; }
-
- public boolean needsOsUpgrade() {
- return parent.wantedOsVersion().isAfter(parent.currentOsVersion());
- }
-
- public boolean needsFirmwareUpgrade(){
- return parent.wantedFirmwareCheck()
- .map(wanted -> parent.currentFirmwareCheck()
- .map(wanted::isAfter)
- .orElse(true))
- .orElse(false);
- }
-
- public boolean hasParentDown() {
- return parent.serviceState() == Node.ServiceState.allowedDown;
- }
-
- public boolean needsPlatformUpgrade() {
- return node.wantedVersion().isAfter(node.currentVersion())
- || ! node.wantedDockerImage().equals(node.currentDockerImage());
- }
-
- public boolean needsReboot() {
- return node.wantedRebootGeneration() > node.rebootGeneration();
- }
-
- public boolean needsRestart() {
- return node.wantedRestartGeneration() > node.restartGeneration();
- }
-
- public boolean isAllowedDown() {
- return node.serviceState() == Node.ServiceState.allowedDown;
- }
-
- public boolean isNewlyProvisioned() {
- return node.currentVersion().equals(Version.emptyVersion);
- }
-
- public boolean isSuspendedSince(Instant instant) {
- return node.suspendedSince().map(instant::isAfter).orElse(false);
- }
-
- public boolean needsNewConfig() {
- return services.stream().anyMatch(service -> wantedConfigGeneration > service.currentGeneration());
- }
-
- public boolean isStateful() {
- return node.clusterType() == Node.ClusterType.content || node.clusterType() == Node.ClusterType.combined;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- NodeWithServices that = (NodeWithServices) o;
- return node.equals(that.node);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(node);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntry.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntry.java
deleted file mode 100644
index f3bf5b2062d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntry.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-
-/**
- * @author mortent
- */
-public class RetriggerEntry {
- private final JobId jobId;
- private final long requiredRun;
-
- public RetriggerEntry(JobId jobId, long requiredRun) {
- this.jobId = jobId;
- this.requiredRun = requiredRun;
- }
-
- public JobId jobId() {
- return jobId;
- }
-
- public long requiredRun() {
- return requiredRun;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntrySerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntrySerializer.java
deleted file mode 100644
index 8ed36215cac..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RetriggerEntrySerializer.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * @author mortent
- */
-public class RetriggerEntrySerializer {
-
- private static final String JOB_ID_KEY = "jobId";
- private static final String APPLICATION_ID_KEY = "applicationId";
- private static final String JOB_TYPE_KEY = "jobType";
- private static final String MIN_REQUIRED_RUN_ID_KEY = "minimumRunId";
-
- public List<RetriggerEntry> fromSlime(Slime slime) {
- return SlimeUtils.entriesStream(slime.get().field("entries"))
- .map(this::deserializeEntry)
- .toList();
- }
-
- public Slime toSlime(List<RetriggerEntry> entryList) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor entries = root.setArray("entries");
- entryList.forEach(e -> serializeEntry(entries, e));
- return slime;
- }
-
- private void serializeEntry(Cursor array, RetriggerEntry entry) {
- Cursor root = array.addObject();
- Cursor jobid = root.setObject(JOB_ID_KEY);
- jobid.setString(APPLICATION_ID_KEY, entry.jobId().application().serializedForm());
- jobid.setString(JOB_TYPE_KEY, entry.jobId().type().serialized());
- root.setLong(MIN_REQUIRED_RUN_ID_KEY, entry.requiredRun());
- }
-
- private RetriggerEntry deserializeEntry(Inspector inspector) {
- Inspector jobid = inspector.field(JOB_ID_KEY);
- ApplicationId applicationId = ApplicationId.fromSerializedForm(require(jobid, APPLICATION_ID_KEY).asString());
- JobType jobType = JobType.ofSerialized(require(jobid, JOB_TYPE_KEY).asString());
- long minRequiredRunId = require(inspector, MIN_REQUIRED_RUN_ID_KEY).asLong();
- return new RetriggerEntry(new JobId(applicationId, jobType), minRequiredRunId);
- }
-
- private Inspector require(Inspector inspector, String fieldName) {
- Inspector field = inspector.field(fieldName);
- if (!field.valid()) {
- throw new IllegalStateException("Could not deserialize, field not found in json: " + fieldName);
- }
- return field;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java
deleted file mode 100644
index 0d086aa7012..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RevisionHistory.java
+++ /dev/null
@@ -1,147 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import ai.vespa.validation.Validation;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-
-import java.util.ArrayDeque;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Deque;
-import java.util.List;
-import java.util.Map;
-import java.util.NavigableMap;
-import java.util.Optional;
-import java.util.OptionalLong;
-import java.util.TreeMap;
-import java.util.function.Predicate;
-
-import static ai.vespa.validation.Validation.require;
-import static java.util.Collections.emptyNavigableMap;
-import static java.util.function.Predicate.not;
-
-/**
- * History of application revisions for an {@link com.yahoo.vespa.hosted.controller.Application}.
- *
- * @author jonmv
- */
-public class RevisionHistory {
-
- private static final Comparator<JobId> comparator = Comparator.comparing(JobId::application).thenComparing(JobId::type);
-
- private final NavigableMap<RevisionId, ApplicationVersion> production;
- private final NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development;
-
- private RevisionHistory(NavigableMap<RevisionId, ApplicationVersion> production,
- NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development) {
- this.production = production;
- this.development = development;
- }
-
- public static RevisionHistory empty() {
- return ofRevisions(List.of(), Map.of());
- }
-
- public static RevisionHistory ofRevisions(Collection<ApplicationVersion> productionRevisions,
- Map<JobId, ? extends Collection<ApplicationVersion>> developmentRevisions) {
- NavigableMap<RevisionId, ApplicationVersion> production = new TreeMap<>();
- for (ApplicationVersion revision : productionRevisions)
- production.put(revision.id(), revision);
-
- NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development = new TreeMap<>(comparator);
- developmentRevisions.forEach((job, jobRevisions) -> {
- NavigableMap<RevisionId, ApplicationVersion> revisions = development.computeIfAbsent(job, __ -> new TreeMap<>());
- for (ApplicationVersion revision : jobRevisions)
- revisions.put(revision.id(), revision);
- });
-
- return new RevisionHistory(production, development);
- }
-
- /** Returns a copy of this where any production revisions without packages, and older than the given one, are removed. */
- public RevisionHistory withoutOlderThan(RevisionId id) {
- if (production.headMap(id).isEmpty()) return this;
- NavigableMap<RevisionId, ApplicationVersion> production = new TreeMap<>(this.production);
- production.headMap(id).values().removeIf(not(ApplicationVersion::hasPackage));
- return new RevisionHistory(production, development);
- }
-
- /** Returns a copy of this without any development revisions older than the given. */
- public RevisionHistory withoutOlderThan(RevisionId id, JobId job) {
- if ( ! development.containsKey(job) || development.get(job).headMap(id).isEmpty()) return this;
- NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development = new TreeMap<>(this.development);
- development.compute(job, (__, revisions) -> revisions.tailMap(id, true));
- return new RevisionHistory(production, development);
- }
-
- /** Returns a copy of this with the revision added or updated. */
- public RevisionHistory with(ApplicationVersion revision) {
- if (revision.id().isProduction()) {
- if ( ! production.isEmpty() && revision.bundleHash().flatMap(hash -> production.lastEntry().getValue().bundleHash().map(hash::equals)).orElse(false))
- revision = revision.skipped();
-
- NavigableMap<RevisionId, ApplicationVersion> production = new TreeMap<>(this.production);
- production.put(revision.id(), revision);
- return new RevisionHistory(production, development);
- }
- else {
- NavigableMap<JobId, NavigableMap<RevisionId, ApplicationVersion>> development = new TreeMap<>(this.development);
- NavigableMap<RevisionId, ApplicationVersion> revisions = development.compute(revision.id().job(), (__, old) -> new TreeMap<>(old != null ? old : emptyNavigableMap()));
- if ( ! revisions.isEmpty()) revisions.compute(revisions.lastKey(), (__, last) -> last.withoutPackage());
- revisions.put(revision.id(), revision);
- return new RevisionHistory(production, development);
- }
- }
-
- // Fallback for when an application version isn't known for the given key.
- private static ApplicationVersion revisionOf(RevisionId id) {
- return new ApplicationVersion(id, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), false, false, Optional.empty(), Optional.empty(), 0);
- }
-
- /** Returns the production {@link ApplicationVersion} with this revision ID. */
- public ApplicationVersion get(RevisionId id) {
- return id.isProduction() ? production.getOrDefault(id, revisionOf(id))
- : development.getOrDefault(id.job(), emptyNavigableMap())
- .getOrDefault(id, revisionOf(id));
- }
-
- /** Returns the last submitted production build. */
- public Optional<ApplicationVersion> last() {
- return Optional.ofNullable(production.lastEntry()).map(Map.Entry::getValue);
- }
-
- /** Returns all known production revisions we still have the package for, from oldest to newest. */
- public List<ApplicationVersion> withPackage() {
- return production.values().stream()
- .filter(ApplicationVersion::hasPackage)
- .toList();
- }
-
- /** Returns the currently deployable revisions of the application. */
- public Deque<ApplicationVersion> deployable(boolean ascending) {
- Deque<ApplicationVersion> versions = new ArrayDeque<>();
- for (ApplicationVersion version : withPackage()) {
- if (version.isDeployable()) {
- if (ascending) versions.addLast(version);
- else versions.addFirst(version);
- }
- }
- return versions;
- }
-
- /** All known production revisions, in ascending order. */
- public List<ApplicationVersion> production() {
- return List.copyOf(production.values());
- }
-
- /* All known development revisions, in ascending order, per job. */
- public NavigableMap<JobId, List<ApplicationVersion>> development() {
- NavigableMap<JobId, List<ApplicationVersion>> copy = new TreeMap<>(comparator);
- development.forEach((job, revisions) -> copy.put(job, List.copyOf(revisions.values())));
- return Collections.unmodifiableNavigableMap(copy);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java
deleted file mode 100644
index 2b207e6662b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Run.java
+++ /dev/null
@@ -1,353 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.Change;
-
-import java.security.cert.X509Certificate;
-import java.time.Instant;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.aborted;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.cancelled;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.noTests;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.success;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished;
-import static java.util.Objects.requireNonNull;
-
-/**
- * Immutable class containing status information for a deployment job run by a {@link JobController}.
- *
- * @author jonmv
- */
-public class Run {
-
- private final RunId id;
- private final Map<Step, StepInfo> steps;
- private final Versions versions;
- private final boolean isRedeployment;
- private final Instant start;
- private final Optional<Instant> end;
- private final Optional<Instant> sleepUntil;
- private final RunStatus status;
- private final long lastTestRecord;
- private final Instant lastVespaLogTimestamp;
- private final Optional<Instant> noNodesDownSince;
- private final Optional<ConvergenceSummary> convergenceSummary;
- private final Optional<X509Certificate> testerCertificate;
- private final boolean dryRun;
- private final Optional<CloudAccount> cloudAccount;
- private final Reason reason;
-
- // For deserialisation only -- do not use!
- public Run(RunId id, Map<Step, StepInfo> steps, Versions versions, boolean isRedeployment, Instant start, Optional<Instant> end,
- Optional<Instant> sleepUntil, RunStatus status, long lastTestRecord, Instant lastVespaLogTimestamp,
- Optional<Instant> noNodesDownSince, Optional<ConvergenceSummary> convergenceSummary,
- Optional<X509Certificate> testerCertificate, boolean dryRun, Optional<CloudAccount> cloudAccount, Reason reason) {
- this.id = id;
- this.steps = Collections.unmodifiableMap(new EnumMap<>(steps));
- this.versions = versions;
- this.isRedeployment = isRedeployment;
- this.start = start;
- this.end = end;
- this.sleepUntil = sleepUntil;
- this.status = status;
- this.lastTestRecord = lastTestRecord;
- this.lastVespaLogTimestamp = lastVespaLogTimestamp;
- this.noNodesDownSince = noNodesDownSince;
- this.convergenceSummary = convergenceSummary;
- this.testerCertificate = testerCertificate;
- this.dryRun = dryRun;
- this.cloudAccount = cloudAccount;
- this.reason = reason;
- }
-
- public static Run initial(RunId id, Versions versions, boolean isRedeployment, Instant now, JobProfile profile, Reason reason) {
- EnumMap<Step, StepInfo> steps = new EnumMap<>(Step.class);
- profile.steps().forEach(step -> steps.put(step, StepInfo.initial(step)));
- return new Run(id, steps, requireNonNull(versions), isRedeployment, requireNonNull(now), Optional.empty(),
- Optional.empty(), running, -1, Instant.EPOCH, Optional.empty(), Optional.empty(),
- Optional.empty(), profile == JobProfile.developmentDryRun, Optional.empty(), reason);
- }
-
- /** Returns a new Run with the status of the given completed step set accordingly. */
- public Run with(RunStatus status, LockedStep step) {
- requireActive();
- StepInfo stepInfo = getRequiredStepInfo(step.get());
- if (stepInfo.status() != unfinished)
- throw new IllegalStateException("Step '" + step.get() + "' can't be set to '" + status + "'" +
- " -- it already completed with status '" + stepInfo.status() + "'!");
-
- EnumMap<Step, StepInfo> steps = new EnumMap<>(this.steps);
- steps.put(step.get(), stepInfo.with(Step.Status.of(status)));
- RunStatus newStatus = hasFailed() || status == running ? this.status : status;
- return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil, newStatus, lastTestRecord,
- lastVespaLogTimestamp, noNodesDownSince, convergenceSummary, testerCertificate, dryRun, cloudAccount, reason);
- }
-
- /** Returns a new Run with a new start time*/
- public Run with(Instant startTime, LockedStep step) {
- requireActive();
- StepInfo stepInfo = getRequiredStepInfo(step.get());
- if (stepInfo.status() != unfinished)
- throw new IllegalStateException("Unable to set start timestamp of step " + step.get() +
- ": it has already completed with status " + stepInfo.status() + "!");
-
- EnumMap<Step, StepInfo> steps = new EnumMap<>(this.steps);
- steps.put(step.get(), stepInfo.with(startTime));
-
- return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, testerCertificate, dryRun, cloudAccount, reason);
- }
-
- public Run finished(Instant now) {
- requireActive();
- return new Run(id, steps, versions, isRedeployment, start, Optional.of(now), sleepUntil, status == running ? success : status,
- lastTestRecord, lastVespaLogTimestamp, noNodesDownSince, convergenceSummary, Optional.empty(), dryRun, cloudAccount, reason);
- }
-
- public Run aborted(boolean cancelledByHumans) {
- requireActive();
- return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil,
- cancelledByHumans ? cancelled : aborted,
- lastTestRecord, lastVespaLogTimestamp, noNodesDownSince,
- convergenceSummary, testerCertificate, dryRun, cloudAccount, reason);
- }
-
- public Run reset() {
- requireActive();
- Map<Step, StepInfo> reset = new EnumMap<>(steps);
- reset.replaceAll((step, __) -> StepInfo.initial(step));
- return new Run(id, reset, versions, isRedeployment, start, end, sleepUntil, running, -1, lastVespaLogTimestamp,
- Optional.empty(), Optional.empty(), testerCertificate, dryRun, cloudAccount, reason);
- }
-
- public Run with(long lastTestRecord) {
- requireActive();
- return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, testerCertificate, dryRun, cloudAccount, reason);
- }
-
- public Run with(Instant lastVespaLogTimestamp) {
- requireActive();
- return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, testerCertificate, dryRun, cloudAccount, reason);
- }
-
- public Run noNodesDownSince(Instant noNodesDownSince) {
- requireActive();
- return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil, status, lastTestRecord, lastVespaLogTimestamp,
- Optional.ofNullable(noNodesDownSince), convergenceSummary, testerCertificate, dryRun, cloudAccount, reason);
- }
-
- public Run withSummary(ConvergenceSummary convergenceSummary) {
- requireActive();
- return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, Optional.ofNullable(convergenceSummary), testerCertificate, dryRun, cloudAccount, reason);
- }
-
- public Run with(X509Certificate testerCertificate) {
- requireActive();
- return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, Optional.of(testerCertificate), dryRun, cloudAccount, reason);
- }
-
- public Run sleepingUntil(Instant instant) {
- requireActive();
- return new Run(id, steps, versions, isRedeployment, start, end, Optional.of(instant), status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, testerCertificate, dryRun, cloudAccount, reason);
- }
-
- public Run with(CloudAccount account) {
- requireActive();
- return new Run(id, steps, versions, isRedeployment, start, end, sleepUntil, status, lastTestRecord, lastVespaLogTimestamp,
- noNodesDownSince, convergenceSummary, testerCertificate, dryRun, Optional.of(account), reason);
- }
-
- /** Returns the id of this run. */
- public RunId id() {
- return id;
- }
-
- /** Whether this run contains this step. */
- public boolean hasStep(Step step) {
- return steps.containsKey(step);
- }
-
- /** Returns info on step, or empty if the given step is not a part of this run. */
- public Optional<StepInfo> stepInfo(Step step) {
- return Optional.ofNullable(steps.get(step));
- }
-
- private StepInfo getRequiredStepInfo(Step step) {
- return stepInfo(step).orElseThrow(() -> new IllegalArgumentException("There is no such step " + step + " for run " + id));
- }
-
- /** Returns status of step, or empty if the given step is not a part of this run. */
- public Optional<Step.Status> stepStatus(Step step) {
- return stepInfo(step).map(StepInfo::status);
- }
-
- /** Returns an unmodifiable view of all step information in this run. */
- public Map<Step, StepInfo> steps() {
- return steps;
- }
-
- /** Returns an unmodifiable view of the status of all steps in this run. */
- public Map<Step, Step.Status> stepStatuses() {
- return Collections.unmodifiableMap(steps.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().status())));
- }
-
- public RunStatus status() {
- return status;
- }
-
- /** Returns the instant at which this run began. */
- public Instant start() {
- return start;
- }
-
- /** Returns the instant at which this run ended, if it has. */
- public Optional<Instant> end() {
- return end;
- }
-
- /** Returns the instant until which this should sleep. */
- public Optional<Instant> sleepUntil() {
- return sleepUntil;
- }
-
- /** Returns whether the run has failed, and should switch to its run-always steps. */
- public boolean hasFailed() {
- return status != running && status != success && status != noTests;
- }
-
- /** Returns whether the run has ended, i.e., has become inactive, and can no longer be updated. */
- public boolean hasEnded() {
- return end.isPresent();
- }
-
- public boolean hasSucceeded() { return hasEnded() && ! hasFailed(); }
-
- /** Returns the target, and possibly source, versions for this run. */
- public Versions versions() {
- return versions;
- }
-
- /** Returns the sequence id of the last test record received from the tester, for the test logs of this run. */
- public long lastTestLogEntry() {
- return lastTestRecord;
- }
-
- /** Returns the timestamp of the last Vespa log record fetched and stored for this run. */
- public Instant lastVespaLogTimestamp() {
- return lastVespaLogTimestamp;
- }
-
- /** Returns since when no nodes have been allowed to be down. */
- public Optional<Instant> noNodesDownSince() {
- return noNodesDownSince;
- }
-
- /** Returns a summary of convergence status during an application deployment — staging or upgrade. */
- public Optional<ConvergenceSummary> convergenceSummary() {
- return convergenceSummary;
- }
-
- /** Returns the tester certificate for this run, or empty. */
- public Optional<X509Certificate> testerCertificate() {
- return testerCertificate;
- }
-
- /** Whether this is a automatic redeployment. */
- public boolean isRedeployment() {
- return isRedeployment;
- }
-
- /** Whether this is a dry run deployment. */
- public boolean isDryRun() { return dryRun; }
-
- /** Cloud account used for deployments in this run. This is set by the first deployment. */
- public Optional<CloudAccount> cloudAccount() { return cloudAccount; }
-
- /** The specific reason for triggering this run, if any. This should be empty for jobs triggered bvy deployment orchestration. */
- public Reason reason() {
- return reason;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if ( ! (o instanceof Run)) return false;
-
- Run run = (Run) o;
-
- return id.equals(run.id);
- }
-
- @Override
- public int hashCode() {
- return id.hashCode();
- }
-
- @Override
- public String toString() {
- return "RunStatus{" +
- "id=" + id +
- ", versions=" + versions +
- ", start=" + start +
- ", end=" + end +
- ", status=" + status +
- '}';
- }
-
- /** Returns the list of steps to run for this job right now, depending on whether the job has failed. */
- public List<Step> readySteps() {
- return hasFailed() ? forcedSteps() : normalSteps();
- }
-
- /** Returns the list of unfinished steps whose prerequisites have all succeeded. */
- private List<Step> normalSteps() {
- return steps.entrySet().stream()
- .filter(entry -> entry.getValue().status() == unfinished
- && entry.getKey().prerequisites().stream()
- .allMatch(step -> steps.get(step) == null
- || steps.get(step).status() == succeeded))
- .map(Map.Entry::getKey)
- .toList();
- }
-
- /** Returns the list of not-yet-run run-always steps whose run-always prerequisites have all run. */
- private List<Step> forcedSteps() {
- return steps.entrySet().stream()
- .filter(entry -> entry.getValue().status() == unfinished
- && entry.getKey().alwaysRun()
- && entry.getKey().prerequisites().stream()
- .filter(Step::alwaysRun)
- .allMatch(step -> steps.get(step) == null
- || steps.get(step).status() != unfinished))
- .map(Map.Entry::getKey)
- .toList();
- }
-
- private void requireActive() {
- if (hasEnded())
- throw new IllegalStateException("This run ended at " + end.get() + " -- it can't be further modified!");
- }
-
- public record Reason(Optional<String> reason, Optional<JobId> dependent, Optional<Change> change) {
- private static final Reason empty = new Reason(Optional.empty(), Optional.empty(), Optional.empty());
- public static Reason empty() { return empty; }
- public static Reason because(String reason) { return new Reason(Optional.of(reason), Optional.empty(), Optional.empty()); }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunList.java
deleted file mode 100644
index b3846dca2c0..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunList.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.collections.AbstractFilteringList;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-
-import java.util.Collection;
-import java.util.List;
-
-/**
- * List for filtering deployment job {@link Run}s.
- *
- * @author jonmv
- */
-public class RunList extends AbstractFilteringList<Run, RunList> {
-
- private RunList(Collection<? extends Run> items, boolean negate) {
- super(items, negate, RunList::new);
- }
-
- public static RunList from(Collection<? extends Run> runs) {
- return new RunList(runs, false);
- }
-
- public static RunList from(JobStatus job) {
- return from(job.runs().descendingMap().values());
- }
-
- /** Returns the jobs with runs matching the given versions — targets only for system test, everything present otherwise. */
- public RunList on(Versions versions) {
- return matching(run -> matchingVersions(run, versions));
- }
-
- /** Returns the runs with status among the given. */
- public RunList status(RunStatus... status) {
- return matching(run -> List.of(status).contains(run.status()));
- }
-
- private static boolean matchingVersions(Run run, Versions versions) {
- return versions.targetsMatch(run.versions())
- && (versions.sourcesMatchIfPresent(run.versions()) || run.id().type().isSystemTest());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunLog.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunLog.java
deleted file mode 100644
index 371607ec1c7..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunLog.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.google.common.collect.ImmutableMap;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.OptionalLong;
-
-/**
- * Immutable class which contains the log of a deployment job run.
- *
- * @author jonmv
- */
-public class RunLog {
-
- private static final RunLog empty = RunLog.of(Collections.emptyMap());
-
- private final Map<Step, List<LogEntry>> log;
- private final OptionalLong lastId;
-
- private RunLog(OptionalLong lastId, Map<Step, List<LogEntry>> log) {
- this.log = log;
- this.lastId = lastId;
- }
-
- /** Creates a RunLog which contains a deep copy of the given log. */
- public static RunLog of(Map<Step, List<LogEntry>> log) {
- ImmutableMap.Builder<Step, List<LogEntry>> builder = ImmutableMap.builder();
- log.forEach((step, entries) -> {
- if ( ! entries.isEmpty())
- builder.put(step, List.copyOf(entries));
- });
- OptionalLong lastId = log.values().stream()
- .flatMap(List::stream)
- .mapToLong(LogEntry::id)
- .max();
- return new RunLog(lastId, builder.build());
- }
-
- /** Returns an empty RunLog. */
- public static RunLog empty() {
- return empty;
- }
-
- /** Returns the log entries for the given step, if any are recorded. */
- public List<LogEntry> get(Step step) {
- return log.getOrDefault(step, Collections.emptyList());
- }
-
- /** Returns the id of the last log entry in this, if it has any. */
- public OptionalLong lastId() {
- return lastId;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java
deleted file mode 100644
index 7e1806ad9ac..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/RunStatus.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-/**
- * Status of jobs run by a {@link JobController}.
- *
- * @author jonmv
- */
-public enum RunStatus {
-
- /** Run is still proceeding normally, i.e., without failures. */
- running,
-
- /** Deployment was rejected due node allocation failure. */
- nodeAllocationFailure,
-
- /** Deployment of the real application was rejected because the package is faulty. */
- invalidApplication,
-
- /** Deployment of the real application was rejected, for other reasons. */
- deploymentFailed,
-
- /** Deployment timed out waiting for endpoint certificate */
- endpointCertificateTimeout,
-
- /** Installation of the real application timed out. */
- installationFailed,
-
- /** The verification tests failed. */
- testFailure,
-
- /** No tests, for a test job. */
- noTests,
-
- /** An unexpected error occurred. */
- error,
-
- /** Everything completed with great success! */
- success,
-
- /** Run was abandoned, due to job timeout or blocking a newer target for the same job. */
- aborted,
-
- /** Cancelled by a human being. */
- cancelled,
-
- /** Run should be reset to its starting state. Used for production tests. */
- reset,
-
- /** Deployment of the real application was rejected due to exceeding quota. */
- quotaExceeded
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java
deleted file mode 100644
index e975f5874f4..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Step.java
+++ /dev/null
@@ -1,122 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.stream.Stream;
-
-import static java.util.Comparator.reverseOrder;
-
-/**
- * Steps that make up a deployment job. See {@link JobProfile} for preset profiles.
- *
- * Each step lists its prerequisites; this serves two purposes:
- *
- * 1. A step may only run after its prerequisites, so these define a topological order in which
- * the steps can be run. Since a job profile may list only a subset of the existing steps,
- * only the prerequisites of a step which are included in a run's profile will be considered.
- * Under normal circumstances, a step will run only after each of its prerequisites have succeeded.
- * When a run has failed, however, each of the always-run steps of the run's profile will be run,
- * again in a topological order, and requiring all their always-run prerequisites to have run.
- *
- * 2. A step will never run concurrently with its prerequisites. This is to ensure, e.g., that relevant
- * information from a failed run is stored, and that deployment does not occur after deactivation.
- *
- * @see JobController
- * @author jonmv
- */
-public enum Step {
-
- /** Download test-jar and assemble and deploy tester application. */
- deployTester(false),
-
- /** See that tester is done deploying, and is ready to serve. */
- installTester(false, deployTester),
-
- /** Download and deploy the initial real application, for staging tests. */
- deployInitialReal(false),
-
- /** See that the real application has had its nodes converge to the initial state. */
- installInitialReal(false, deployInitialReal),
-
- /** Ask the tester to run its staging setup. */
- startStagingSetup(false, installInitialReal, installTester),
-
- /** See that the staging setup is done. */
- endStagingSetup(false, startStagingSetup),
-
- /** Download and deploy real application, restarting services if required. */
- deployReal(false, endStagingSetup),
-
- /** See that real application has had its nodes converge to the wanted version and generation. */
- installReal(false, deployReal),
-
- /** Ask the tester to run its tests. */
- startTests(false, installReal, installTester),
-
- /** See that the tests are done running. */
- endTests(false, startTests),
-
- /** Fetch and store Vespa logs from the log server cluster of the deployment -- used for test and dev deployments. */
- copyVespaLogs(true, installReal, endTests),
-
- /** Delete the real application -- used for test deployments. */
- deactivateReal(true, deployInitialReal, deployReal, endTests, copyVespaLogs),
-
- /** Deactivate the tester. */
- deactivateTester(true, deployTester, endTests, copyVespaLogs),
-
- /** Report completion to the deployment orchestration machinery. */
- report(true, installReal, deactivateReal, deactivateTester);
-
-
- private final boolean alwaysRun;
- private final List<Step> prerequisites;
-
- Step(boolean alwaysRun, Step... prerequisites) {
- this.alwaysRun = alwaysRun;
- this.prerequisites = List.of(prerequisites);
- }
-
- /** Returns whether this is a cleanup-step, and should always run, regardless of job outcome, when specified in a job. */
- public boolean alwaysRun() { return alwaysRun; }
-
- /** Returns all prerequisite steps for this, including transient ones, in a job profile containing the given steps. */
- public List<Step> allPrerequisites(Collection<Step> among) {
- return prerequisites.stream()
- .filter(among::contains)
- .flatMap(pre -> Stream.concat(Stream.of(pre),
- pre.allPrerequisites(among).stream()))
- .sorted(reverseOrder())
- .distinct()
- .toList();
- }
-
-
- /** Returns the direct prerequisite steps that must be completed before this, assuming the job contains these steps. */
- public List<Step> prerequisites() { return prerequisites; }
-
-
- public enum Status {
-
- /** Step still has unsatisfied finish criteria -- it may not even have started. */
- unfinished,
-
- /** Step failed and subsequent steps may not start. */
- failed,
-
- /** Step succeeded and subsequent steps may now start. */
- succeeded;
-
- public static Step.Status of(RunStatus status) {
- return switch (status) {
- case success -> throw new AssertionError("Unexpected run status '" + status + "'!");
- case cancelled, reset, aborted -> unfinished;
- case noTests, running -> succeeded;
- default -> failed;
- };
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/StepInfo.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/StepInfo.java
deleted file mode 100644
index 60743e45434..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/StepInfo.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import java.time.Instant;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Information about a step. Immutable.
- *
- * @author hakonhall
- */
-public class StepInfo {
-
- private final Step step;
- private final Step.Status status;
- private final Optional<Instant> startTime;
-
- public static StepInfo initial(Step step) { return new StepInfo(step, Step.Status.unfinished, Optional.empty()); }
-
- public StepInfo(Step step, Step.Status status, Optional<Instant> startTime) {
- this.step = step;
- this.status = status;
- this.startTime = startTime;
- }
-
- public Step step() { return step; }
- public Step.Status status() { return status; }
- public Optional<Instant> startTime() { return startTime; }
-
- /** Returns a copy of this, but with the given status. */
- public StepInfo with(Step.Status status) { return new StepInfo(step, status, startTime); }
-
- /** Returns a copy of this, but with the given start timestamp. */
- public StepInfo with(Instant startTimestamp) { return new StepInfo(step, status, Optional.of(startTimestamp)); }
-
- @Override
- public String toString() {
- return "StepInfo{" +
- "step=" + step +
- ", status=" + status +
- ", startTime=" + startTime +
- '}';
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- StepInfo stepInfo = (StepInfo) o;
- return step == stepInfo.step &&
- status == stepInfo.status &&
- Objects.equals(startTime, stepInfo.startTime);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(step, status, startTime);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/StepRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/StepRunner.java
deleted file mode 100644
index 87df1e925f0..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/StepRunner.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-
-import java.util.Optional;
-
-/**
- * Advances a given job run by running the appropriate {@link Step}s, based on their current status.
- *
- * When an attempt is made to advance a given job, a lock for that job (application and type) is
- * taken, and released again only when the attempt finishes. Multiple other attempts may be made in
- * the meantime, but they should give up unless the lock is promptly acquired.
- *
- * @author jonmv
- */
-public interface StepRunner {
-
- /** Attempts to run the given step in the given run, and returns the new status of the run, if the step completed. */
- Optional<RunStatus> run(LockedStep step, RunId id);
-
-}
-
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Submission.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Submission.java
deleted file mode 100644
index ce346f5ba74..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Submission.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-
-import java.time.Instant;
-import java.util.Optional;
-
-import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.calculateHash;
-
-/**
- * @author jonmv
- */
-public class Submission {
-
- private final ApplicationPackage applicationPackage;
- private final byte[] testPackage;
- private final Optional<String> sourceUrl;
- private final Optional<SourceRevision> source;
- private final Optional<String> authorEmail;
- private final Optional<String> description;
- private final Instant now;
- private final int risk;
-
- public Submission(ApplicationPackage applicationPackage, byte[] testPackage, Optional<String> sourceUrl,
- Optional<SourceRevision> source, Optional<String> authorEmail, Optional<String> description,
- Instant now, int risk) {
- this.applicationPackage = applicationPackage;
- this.testPackage = testPackage;
- this.sourceUrl = sourceUrl;
- this.source = source;
- this.authorEmail = authorEmail;
- this.description = description;
- this.now = now;
- this.risk = risk;
- }
-
- public ApplicationVersion toApplicationVersion(long number) {
- return ApplicationVersion.forProduction(RevisionId.forProduction(number),
- source,
- authorEmail,
- applicationPackage.compileVersion(),
- applicationPackage.deploymentSpec().majorVersion(),
- applicationPackage.buildTime(),
- sourceUrl,
- source.map(SourceRevision::commit),
- Optional.of(applicationPackage.bundleHash() + calculateHash(testPackage)),
- description,
- now,
- risk);
- }
-
- public ApplicationPackage applicationPackage() { return applicationPackage; }
-
- public byte[] testPackage() { return testPackage; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java
deleted file mode 100644
index a5a91e7cdd2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializer.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.time.Instant;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Serializes config for integration tests against Vespa deployments.
- *
- * @author jonmv
- */
-public class TestConfigSerializer {
-
- private final SystemName system;
-
- public TestConfigSerializer(SystemName system) {
- this.system = system;
- }
-
- public Slime configSlime(ApplicationId id,
- JobType type,
- boolean isCI,
- Version platform,
- RevisionId revision,
- Instant deployedAt,
- Map<ZoneId, List<Endpoint>> deployments,
- Map<ZoneId, List<String>> clusters) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
-
- root.setString("application", id.serializedForm());
- root.setString("zone", type.zone().value());
- root.setString("system", system.value());
- root.setBool("isCI", isCI);
- root.setString("platform", platform.toFullString());
- root.setLong("revision", revision.number());
- root.setLong("deployedAt", deployedAt.toEpochMilli());
-
- // TODO jvenstad: remove when clients can be updated
- Cursor endpointsObject = root.setObject("endpoints");
- deployments.forEach((zone, endpoints) -> {
- Cursor endpointArray = endpointsObject.setArray(zone.value());
- for (Endpoint endpoint : endpoints)
- endpointArray.addString(endpoint.url().toString());
- });
-
- Cursor zoneEndpointsObject = root.setObject("zoneEndpoints");
- deployments.forEach((zone, endpoints) -> {
- Cursor clusterEndpointsObject = zoneEndpointsObject.setObject(zone.value());
- for (Endpoint endpoint : endpoints)
- clusterEndpointsObject.setString(endpoint.name(), endpoint.url().toString());
- });
-
- if ( ! clusters.isEmpty()) {
- Cursor clustersObject = root.setObject("clusters");
- clusters.forEach((zone, clusterList) -> {
- Cursor clusterArray = clustersObject.setArray(zone.value());
- for (String cluster : clusterList)
- clusterArray.addString(cluster);
- });
- }
-
- return slime;
- }
-
- /** Returns the config for the tests to run for the given job. */
- public byte[] configJson(ApplicationId id,
- JobType type,
- boolean isCI,
- Version platform,
- RevisionId revision,
- Instant deployedAt,
- Map<ZoneId, List<Endpoint>> deployments,
- Map<ZoneId, List<String>> clusters) {
- try {
- return SlimeUtils.toJsonBytes(configSlime(id, type, isCI, platform, revision, deployedAt, deployments, clusters));
- }
- catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java
deleted file mode 100644
index 9b4fbf06e21..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/Versions.java
+++ /dev/null
@@ -1,156 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.function.Supplier;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Source and target versions for an application.
- *
- * @author jvenstad
- * @author mpolden
- */
-public class Versions {
-
- private final Version targetPlatform;
- private final RevisionId targetRevision;
- private final Optional<Version> sourcePlatform;
- private final Optional<RevisionId> sourceRevision;
-
- public Versions(Version targetPlatform, RevisionId targetRevision, Optional<Version> sourcePlatform,
- Optional<RevisionId> sourceRevision) {
- if (sourcePlatform.isPresent() ^ sourceRevision.isPresent())
- throw new IllegalArgumentException("Sources must both be present or absent.");
-
- this.targetPlatform = requireNonNull(targetPlatform);
- this.targetRevision = requireNonNull(targetRevision);
- this.sourcePlatform = requireNonNull(sourcePlatform);
- this.sourceRevision = requireNonNull(sourceRevision);
- }
-
- /** A copy of this, without source versions. */
- public Versions withoutSources() {
- return new Versions(targetPlatform, targetRevision, Optional.empty(), Optional.empty());
- }
-
- /** Target platform version for this */
- public Version targetPlatform() {
- return targetPlatform;
- }
-
- /** Target revision for this */
- public RevisionId targetRevision() {
- return targetRevision;
- }
-
- /** Source platform version for this */
- public Optional<Version> sourcePlatform() {
- return sourcePlatform;
- }
-
- /** Source application version for this */
- public Optional<RevisionId> sourceRevision() {
- return sourceRevision;
- }
-
- /** Returns whether source versions are present and match those of the given job other versions. */
- public boolean sourcesMatchIfPresent(Versions versions) {
- return (sourcePlatform.map(targetPlatform::equals).orElse(true) ||
- sourcePlatform.equals(versions.sourcePlatform())) &&
- (sourceRevision.map(targetRevision::equals).orElse(true) ||
- sourceRevision.equals(versions.sourceRevision()));
- }
-
- public boolean targetsMatch(Versions versions) {
- return targetPlatform.equals(versions.targetPlatform()) &&
- targetRevision.equals(versions.targetRevision());
- }
-
- /** Returns whether this change could result in the given target versions. */
- public boolean targetsMatch(Change change) {
- return change.platform().map(targetPlatform::equals).orElse(true)
- && change.revision().map(targetRevision::equals).orElse(true);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if ( ! (o instanceof Versions)) return false;
- Versions versions = (Versions) o;
- return Objects.equals(targetPlatform, versions.targetPlatform) &&
- Objects.equals(targetRevision, versions.targetRevision) &&
- Objects.equals(sourcePlatform, versions.sourcePlatform) &&
- Objects.equals(sourceRevision, versions.sourceRevision);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(targetPlatform, targetRevision, sourcePlatform, sourceRevision);
- }
-
- @Override
- public String toString() {
- return Text.format("platform %s%s, revision %s%s",
- sourcePlatform.filter(source -> ! source.equals(targetPlatform))
- .map(source -> source + " -> ").orElse(""),
- targetPlatform,
- sourceRevision.filter(source -> ! source.equals(targetRevision))
- .map(source -> source + " -> ").orElse(""),
- targetRevision);
- }
-
- /** Create versions using given change and application */
- public static Versions from(Change change, Application application, Optional<Version> existingPlatform,
- Optional<RevisionId> existingRevision, Supplier<Version> defaultPlatformVersion) {
- return new Versions(targetPlatform(application, change, existingPlatform, defaultPlatformVersion),
- targetRevision(application, change, existingRevision),
- existingPlatform,
- existingRevision);
- }
-
- /** Create versions using given change and application */
- public static Versions from(Change change, Application application, Optional<Deployment> deployment, Supplier<Version> defaultPlatformVersion) {
- return from(change, application, deployment.map(Deployment::version), deployment.map(Deployment::revision), defaultPlatformVersion);
- }
-
- private static Version targetPlatform(Application application, Change change, Optional<Version> existing,
- Supplier<Version> defaultVersion) {
- if (change.isPlatformPinned() && change.platform().isPresent())
- return change.platform().get();
-
- return max(change.platform(), existing)
- .orElseGet(() -> application.oldestDeployedPlatform().orElseGet(defaultVersion));
- }
-
- private static RevisionId targetRevision(Application application, Change change,
- Optional<RevisionId> existing) {
- if (change.isRevisionPinned() && change.revision().isPresent())
- return change.revision().get();
-
- return change.revision()
- .or(() -> existing)
- .orElseGet(() -> defaultRevision(application));
- }
-
- private static RevisionId defaultRevision(Application application) {
- return application.oldestDeployedRevision()
- .or(() -> application.revisions().last().map(ApplicationVersion::id))
- .orElseThrow(() -> new IllegalStateException("no known prod revisions, but asked for one, for " + application));
- }
-
- private static <T extends Comparable<T>> Optional<T> max(Optional<T> o1, Optional<T> o2) {
- return o1.isEmpty() ? o2 : o2.isEmpty() ? o1 : o1.get().compareTo(o2.get()) >= 0 ? o1 : o2;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/ZipBuilder.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/ZipBuilder.java
deleted file mode 100644
index 17d347bda17..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/ZipBuilder.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.function.Predicate;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-import java.util.zip.ZipOutputStream;
-
-/**
- * Utility class to build zipped content by adding already zipped byte content or
- * adding new unzipped entries.
- *
- * @author freva
- */
-public class ZipBuilder implements AutoCloseable {
-
- private final ByteArrayOutputStream byteArrayOutputStream;
- private final ZipOutputStream zipOutputStream;
-
- public ZipBuilder(int initialSize) {
- byteArrayOutputStream = new ByteArrayOutputStream(initialSize);
- zipOutputStream = new ZipOutputStream(byteArrayOutputStream);
- }
-
- public void add(byte[] zippedContent, Predicate<String> filter) {
- try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zippedContent))) {
- for (ZipEntry entry = zin.getNextEntry(); entry != null; entry = zin.getNextEntry()) {
- if ( ! filter.test(entry.getName())) continue;
- zipOutputStream.putNextEntry(new ZipEntry(entry.getName()));
- zin.transferTo(zipOutputStream);
- zipOutputStream.closeEntry();
- }
- } catch (IOException e) {
- throw new UncheckedIOException("Failed to add zipped content", e);
- }
- }
-
- public void add(String entryName, byte[] content) {
- try {
- zipOutputStream.putNextEntry(new ZipEntry(entryName));
- zipOutputStream.write(content);
- zipOutputStream.closeEntry();
- } catch (IOException e) {
- throw new UncheckedIOException("Failed to add entry " + entryName, e);
- }
- }
-
- /** @return zipped byte array */
- public byte[] toByteArray() {
- return byteArrayOutputStream.toByteArray();
- }
-
- @Override
- public void close() {
- try {
- zipOutputStream.close();
- } catch (IOException e) {
- throw new UncheckedIOException("Failed to close zip output stream", e);
- }
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/package-info.java
deleted file mode 100644
index 4619f5d32c2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/deployment/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/AbstractNameServiceRequest.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/AbstractNameServiceRequest.java
deleted file mode 100644
index f4223ad90bc..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/AbstractNameServiceRequest.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.dns;
-
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-
-import java.util.Optional;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * @author jonmv
- */
-public abstract class AbstractNameServiceRequest implements NameServiceRequest {
-
- private final Optional<TenantAndApplicationId> owner;
- private final RecordName name;
-
- AbstractNameServiceRequest(Optional<TenantAndApplicationId> owner, RecordName name) {
- this.owner = requireNonNull(owner);
- this.name = requireNonNull(name);
- }
-
- @Override
- public RecordName name() {
- return name;
- }
-
- @Override
- public Optional<TenantAndApplicationId> owner() {
- return owner;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecord.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecord.java
deleted file mode 100644
index c4c76bc7954..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecord.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.dns;
-
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Create or update a record.
- *
- * @author mpolden
- */
-public class CreateRecord extends AbstractNameServiceRequest {
-
- private final Record record;
-
- /** DO NOT USE. Public for serialization purposes */
- public CreateRecord(Optional<TenantAndApplicationId> owner, Record record) {
- super(owner, record.name());
- this.record = Objects.requireNonNull(record, "record must be non-null");
- if (record.type() != Record.Type.CNAME && record.type() != Record.Type.A) {
- throw new IllegalArgumentException("Record of type " + record.type() + " is not supported: " + record);
- }
- }
-
- public Record record() {
- return record;
- }
-
- @Override
- public void dispatchTo(NameService nameService) {
- List<Record> records = nameService.findRecords(record.type(), record.name());
- records.forEach(r -> {
- // Ensure that existing record has correct data
- if (!r.data().equals(record.data())) {
- nameService.updateRecord(r, record.data());
- }
- });
- if (records.isEmpty()) {
- nameService.createRecord(record.type(), record.name(), record.data());
- }
- }
-
- @Override
- public String toString() {
- return "create record " + record;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- CreateRecord that = (CreateRecord) o;
- return owner().equals(that.owner()) && record.equals(that.record);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(owner(), record);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java
deleted file mode 100644
index d560dbd8db9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/CreateRecords.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.dns;
-
-import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-/**
- * Create or update multiple records of the same type and name.
- *
- * @author mpolden
- */
-public class CreateRecords extends AbstractNameServiceRequest {
-
- private final Record.Type type;
- private final List<Record> records;
-
- /** DO NOT USE. Public for serialization purposes */
- public CreateRecords(Optional<TenantAndApplicationId> owner, List<Record> records) {
- super(owner, requireOneOf(Record::name, records));
- this.type = requireOneOf(Record::type, records);
- this.records = List.copyOf(Objects.requireNonNull(records, "records must be non-null"));
- if (type != Record.Type.ALIAS && type != Record.Type.TXT && type != Record.Type.DIRECT) {
- throw new IllegalArgumentException("Records of type " + type + " are not supported: " + records);
- }
- }
-
- public List<Record> records() {
- return records;
- }
-
- @Override
- public void dispatchTo(NameService nameService) {
- switch (type) {
- case ALIAS -> {
- var targets = records.stream().map(Record::data).map(AliasTarget::unpack).collect(Collectors.toSet());
- nameService.createAlias(name(), targets);
- }
- case DIRECT -> {
- var targets = records.stream().map(Record::data).map(DirectTarget::unpack).collect(Collectors.toSet());
- nameService.createDirect(name(), targets);
- }
- case TXT -> {
- var dataFields = records.stream().map(Record::data).toList();
- nameService.createTxtRecords(name(), dataFields);
- }
- }
- }
-
- @Override
- public String toString() {
- return "create records " + records();
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- CreateRecords that = (CreateRecords) o;
- return owner().equals(that.owner()) && records.equals(that.records);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(owner(), records);
- }
-
- /** Find exactly one distinct value of field in given list */
- private static <T, V> T requireOneOf(Function<V, T> field, List<V> list) {
- Set<T> values = list.stream().map(field).collect(Collectors.toSet());
- if (values.size() != 1) {
- throw new IllegalArgumentException("Expected one distinct value, but found " + values + " in " + list);
- }
- return values.iterator().next();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java
deleted file mode 100644
index 40d2667b9ae..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceForwarder.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.dns;
-
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.maintenance.NameServiceDispatcher;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * This adds name service requests to the {@link NameServiceQueue}.
- *
- * Name service requests passed to this are not immediately sent to a name service, but are instead persisted
- * in a curator-backed queue. Enqueued requests are later dispatched to a {@link NameService} by
- * {@link NameServiceDispatcher}.
- *
- * @author mpolden
- */
-public class NameServiceForwarder {
-
- private static final Logger log = Logger.getLogger(NameServiceForwarder.class.getName());
-
- private final CuratorDb db;
-
- public NameServiceForwarder(CuratorDb db) {
- this.db = Objects.requireNonNull(db, "db must be non-null");
- }
-
- /** Create or update a given record */
- public void createRecord(Record record, NameServiceQueue.Priority priority, Optional<TenantAndApplicationId> owner) {
- forward(new CreateRecord(owner, record), priority);
- }
-
- /** Create or update an ALIAS record with given name and targets */
- public void createAlias(RecordName name, Set<AliasTarget> targets, NameServiceQueue.Priority priority, Optional<TenantAndApplicationId> owner) {
- var records = targets.stream()
- .map(target -> new Record(Record.Type.ALIAS, name, target.pack()))
- .toList();
- forward(new CreateRecords(owner, records), priority);
- }
-
- /** Create or update a DIRECT record with given name and targets */
- public void createDirect(RecordName name, Set<DirectTarget> targets, NameServiceQueue.Priority priority, Optional<TenantAndApplicationId> owner) {
- var records = targets.stream()
- .map(target -> new Record(Record.Type.DIRECT, name, target.pack()))
- .toList();
- forward(new CreateRecords(owner, records), priority);
- }
-
- /** Create or update a TXT record with given name and data */
- public void createTxt(RecordName name, List<RecordData> txtData, NameServiceQueue.Priority priority, Optional<TenantAndApplicationId> owner) {
- var records = txtData.stream()
- .map(data -> new Record(Record.Type.TXT, name, data))
- .toList();
- forward(new CreateRecords(owner, records), priority);
- }
-
- /** Remove all records of given type and name */
- public void removeRecords(Record.Type type, RecordName name, NameServiceQueue.Priority priority, Optional<TenantAndApplicationId> owner) {
- forward(new RemoveRecords(owner, type, name), priority);
- }
-
- /** Remove all records of given type, name and data */
- public void removeRecords(Record.Type type, RecordName name, RecordData data, NameServiceQueue.Priority priority, Optional<TenantAndApplicationId> owner) {
- forward(new RemoveRecords(owner, type, name, data), priority);
- }
-
- protected void forward(NameServiceRequest request, NameServiceQueue.Priority priority) {
- try (Mutex lock = db.lockNameServiceQueue()) {
- NameServiceQueue queue = db.readNameServiceQueue();
- var queued = queue.requests().size();
- if (queued > NameServiceQueue.QUEUE_CAPACITY) {
- log.log(Level.WARNING, "Queue is above capacity (size: " + queued + "), failing requests will be dropped. " +
- "This likely means that the name service is not successfully executing requests");
- }
- log.log(Level.FINE, () -> "Queueing name service request: " + request);
- db.writeNameServiceQueue(queue.with(request, priority));
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceQueue.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceQueue.java
deleted file mode 100644
index 033a019f35f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceQueue.java
+++ /dev/null
@@ -1,189 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.dns;
-
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.yolean.Exceptions;
-
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.function.UnaryOperator;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * A queue of outstanding {@link NameServiceRequest}s. Requests in this have not yet been dispatched to a
- * {@link NameService} and are thus not visible in DNS.
- *
- * This is immutable.
- *
- * @author mpolden
- * @author jonmv
- */
-public record NameServiceQueue(List<NameServiceRequest> requests) {
-
- public static final NameServiceQueue EMPTY = new NameServiceQueue(List.of());
-
- /**
- * The number of {@link NameServiceRequest}s we allow to be queued. When the queue overflows, failing requests
- * are dropped in a FIFO order until the queue shrinks below this capacity. If that is not enough, the oldest
- * requests will also be dropped, as needed.
- */
- static final int QUEUE_CAPACITY = 400;
-
- private static final Logger log = Logger.getLogger(NameServiceQueue.class.getName());
-
- /** DO NOT USE. Public for serialization purposes */
- public NameServiceQueue(List<NameServiceRequest> requests) {
- this.requests = List.copyOf(Objects.requireNonNull(requests, "requests must be non-null"));
- }
-
- /** Returns a copy of this containing the last n requests */
- public NameServiceQueue last(int n) {
- return resize(n, (requests) -> requests.subList(requests.size() - n, requests.size()));
- }
-
- /** Returns a copy of this containing the first n requests */
- public NameServiceQueue first(int n) {
- return resize(n, (requests) -> requests.subList(0, n));
- }
-
- /** Returns a copy of this with given request queued according to priority */
- public NameServiceQueue with(NameServiceRequest request, Priority priority) {
- List<NameServiceRequest> copy = new ArrayList<>(this.requests.size() + 1);
- switch (priority) {
- case normal -> {
- copy.addAll(this.requests);
- copy.add(request);
- }
- case high -> {
- copy.add(request);
- copy.addAll(this.requests);
- }
- }
- return new NameServiceQueue(copy);
- }
-
- /** Returns a copy of this without the requests present in other. Duplicates are not removed */
- public NameServiceQueue without(NameServiceQueue other) {
- List<NameServiceRequest> toRemove = new ArrayList<>(other.requests);
- return new NameServiceQueue(requests.stream()
- .filter(request -> !toRemove.remove(request))
- .toList());
- }
-
- /** Returns a copy of this with given request added */
- public NameServiceQueue with(NameServiceRequest request) {
- return with(request, Priority.normal);
- }
-
- /**
- * Dispatch n requests from the head of this to given name service. Requests may be re-ordered if errors are
- * encountered, but are always dispatched in order within an application.
- *
- * @return A copy of this, without the successfully dispatched requests.
- */
- public NameServiceQueue dispatchTo(NameService nameService, int n) {
- requireNonNegative(n);
- if (requests.isEmpty()) return this;
-
- LinkedList<NameServiceRequest> pending = new LinkedList<>(requests);
- while (n-- > 0 && ! pending.isEmpty()) {
- NameServiceRequest request = pending.poll();
- try {
- request.dispatchTo(nameService);
- } catch (Exception e) {
- boolean dropFailingRequest = pending.size() > QUEUE_CAPACITY;
- log.log(Level.WARNING, "Failed to execute " + request + ": " + Exceptions.toMessageString(e) +
- ", request will " + (dropFailingRequest ? "be dropped, as queue is over capacity"
- : "be moved backwards, and retried"));
- if (dropFailingRequest) continue; // Drop this request and keep dispatching others
-
- // Move all requests with the same owner backwards as far as we can, i.e., to the back, or to the first owner-less request.
- Optional<TenantAndApplicationId> owner = request.owner();
- LinkedList<NameServiceRequest> owned = new LinkedList<>();
- LinkedList<NameServiceRequest> others = new LinkedList<>();
- do {
- if (request.owner().isEmpty()) {
- pending.push(request);
- break; // Can't modify anything past this, as owner-less requests must come in order with all others.
- }
- (request.owner().equals(owner) ? owned : others).offer(request);
- } while ((request = pending.poll()) != null);
- pending.addAll(0, owned); // Append owned requests before those we can't modify (or none), and
- pending.addAll(0, others); // then append requests owned by others before that again.
- }
- }
-
- NameServiceQueue remaining = new NameServiceQueue(pending);
- if (pending.size() > 2 * QUEUE_CAPACITY) {
- log.log(Level.WARNING, "Queue has " + pending.size() + " entries, and must be emptying far too slowly; " +
- "dropping the oldest entries past " + 2 * QUEUE_CAPACITY);
- remaining = remaining.last(2 * QUEUE_CAPACITY);
- }
- return remaining;
- }
-
- @Override
- public String toString() {
- return requests.toString();
- }
-
- private NameServiceQueue resize(int n, UnaryOperator<List<NameServiceRequest>> resizer) {
- requireNonNegative(n);
- if (requests.size() <= n) return this;
- return new NameServiceQueue(resizer.apply(requests));
- }
-
- private static void requireNonNegative(int n) {
- if (n < 0) throw new IllegalArgumentException("n must be >= 0, got " + n);
- }
-
- /**
- * Replaces the requests in {@code oldQueue} contained in this with requests in {@code newQueue}, or best effort
- * amendment when not contained.
- */
- public NameServiceQueue replace(NameServiceQueue oldQueue, NameServiceQueue newQueue) {
- int sublistIndex = indexOf(oldQueue.requests, requests);
- if (sublistIndex >= 0) {
- List<NameServiceRequest> updated = new ArrayList<>();
- updated.addAll(requests.subList(0, sublistIndex));
- updated.addAll(newQueue.requests);
- updated.addAll(requests.subList(sublistIndex + oldQueue.requests.size(), requests.size()));
- return new NameServiceQueue(updated);
- } else {
- log.log(Level.WARNING, "Name service queue has changed unexpectedly; expected requests: " +
- oldQueue.requests + " to be present, but that was not found in: " + requests);
- // Do a best-effort amendment, where requests removed from initial to remaining, are removed, from the front, from this.
- return without(oldQueue.without(newQueue));
- }
- }
-
- /**
- * Find the starting index of subList in list. I.e. the lowest index {@code i} in {@code list} so that
- * {@code list.subList(i, i + subList.size()).equals(subList)}. Naïve implementation.
- */
- private static <T> int indexOf(List<T> subList, List<T> list) {
- for (int i = 0; i + subList.size() <= list.size(); i++) {
- if (list.subList(i, i + subList.size()).equals(subList)) {
- return i;
- }
- }
- return -1;
- }
-
- /** Priority of a request added to this */
- public enum Priority {
-
- /** Default priority. Request will be delivered in FIFO order */
- normal,
-
- /** Request is queued first. Useful for code that needs to act on effects of a request */
- high
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceRequest.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceRequest.java
deleted file mode 100644
index d86c2ce565b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/NameServiceRequest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.dns;
-
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-
-import java.util.Optional;
-
-/**
- * Interface for requests to a {@link NameService}.
- *
- * @author mpolden
- */
-public interface NameServiceRequest {
-
- /** The record name this request pertains to. */
- RecordName name();
-
- /** The application owning this request */
- Optional<TenantAndApplicationId> owner();
-
- /** Send this to given name service */
- void dispatchTo(NameService nameService);
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java
deleted file mode 100644
index 0ed835f32bd..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/dns/RemoveRecords.java
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.dns;
-
-import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Permanently removes all matching records by type and matching either:
- *
- * - name and data
- * - only name
- *
- * @author mpolden
- */
-public class RemoveRecords extends AbstractNameServiceRequest {
-
- private final Record.Type type;
- private final Optional<RecordData> data;
-
- public RemoveRecords(Optional<TenantAndApplicationId> owner, Record.Type type, RecordName name) {
- this(owner, type, name, Optional.empty());
- }
-
- public RemoveRecords(Optional<TenantAndApplicationId> owner, Record.Type type, RecordName name, RecordData data) {
- this(owner, type, name, Optional.of(data));
- }
-
- /** DO NOT USE. Public for serialization purposes */
- public RemoveRecords(Optional<TenantAndApplicationId> owner, Record.Type type, RecordName name, Optional<RecordData> data) {
- super(owner, name);
- this.type = Objects.requireNonNull(type, "type must be non-null");
- this.data = Objects.requireNonNull(data, "data must be non-null");
- }
-
- public Record.Type type() {
- return type;
- }
-
- public Optional<RecordData> data() {
- return data;
- }
-
- @Override
- public void dispatchTo(NameService nameService) {
- // Deletions require all records fields to match exactly, data may be incomplete even if present. To ensure
- // completeness we search for the record(s) first
- List<Record> completeRecords = nameService.findRecords(type, name()).stream()
- .filter(record -> data.isEmpty() || matchingFqdnIn(data.get(), record))
- .toList();
- nameService.removeRecords(completeRecords);
- }
-
- @Override
- public String toString() {
- return "remove records of type " + type + ", by name " + name() +
- data.map(d -> " data " + d).orElse("");
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- RemoveRecords that = (RemoveRecords) o;
- return owner().equals(that.owner()) && type == that.type && name().equals(that.name()) && data.equals(that.data);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(owner(), type, name(), data);
- }
-
- private static boolean matchingFqdnIn(RecordData data, Record record) {
- String dataValue = switch (record.type()) {
- case ALIAS -> AliasTarget.unpack(record.data()).name().value();
- case DIRECT -> DirectTarget.unpack(record.data()).recordData().asString();
- default -> record.data().asString();
- };
- return fqdn(dataValue).equals(fqdn(data.asString()));
- }
-
- private static String fqdn(String name) {
- return name.endsWith(".") ? name : name + ".";
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationMetaDataGarbageCollector.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationMetaDataGarbageCollector.java
deleted file mode 100644
index 29e251a9de3..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationMetaDataGarbageCollector.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-
-import java.time.Duration;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * @author jvenstad
- */
-public class ApplicationMetaDataGarbageCollector extends ControllerMaintainer {
-
- private static final Logger log = Logger.getLogger(ApplicationMetaDataGarbageCollector.class.getName());
-
- private final Duration timeToLive;
-
- public ApplicationMetaDataGarbageCollector(Controller controller, Duration interval) {
- super(controller, interval);
- this.timeToLive = controller.system().isCd() ? Duration.ofDays(7) : Duration.ofDays(365);
- }
-
- @Override
- protected double maintain() {
- try {
- controller().applications().applicationStore().pruneMeta(controller().clock().instant().minus(timeToLive));
- return 1.0;
- }
- catch (Exception e) {
- log.log(Level.WARNING, "Exception pruning old application meta data", e);
- return 0.0;
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java
deleted file mode 100644
index d998413e675..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmer.java
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.AccountId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.ApplicationSummary;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.HashMap;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.logging.Level;
-
-/**
- * Periodically request application ownership confirmation through filing issues.
- *
- * When to file new issues, escalate inactive ones, etc., is handled by the enclosed OwnershipIssues.
- *
- * @author jonmv
- */
-public class ApplicationOwnershipConfirmer extends ControllerMaintainer {
-
- private final OwnershipIssues ownershipIssues;
- private final ApplicationController applications;
- private final int shards;
-
- public ApplicationOwnershipConfirmer(Controller controller, Duration interval, OwnershipIssues ownershipIssues) {
- this(controller, interval, ownershipIssues, 24);
- }
-
- public ApplicationOwnershipConfirmer(Controller controller, Duration interval, OwnershipIssues ownershipIssues, int shards) {
- super(controller, interval);
- this.ownershipIssues = ownershipIssues;
- this.applications = controller.applications();
- if (shards <= 0) throw new IllegalArgumentException("shards must be a positive number, but got " + shards);
- this.shards = shards;
- }
-
- @Override
- protected double maintain() {
- return ( confirmApplicationOwnerships() +
- ensureConfirmationResponses() +
- updateConfirmedApplicationOwners() )
- / 3;
- }
-
- /** File an ownership issue with the owners of all applications we know about. */
- private double confirmApplicationOwnerships() {
- AtomicInteger attempts = new AtomicInteger(0);
- AtomicInteger failures = new AtomicInteger(0);
- applications()
- .withProjectId()
- .withProductionDeployment()
- .asList()
- .stream()
- .filter(application -> application.createdAt().isBefore(controller().clock().instant().minus(Duration.ofDays(90))))
- .filter(application -> isInCurrentShard(application.id()))
- .forEach(application -> {
- try {
- attempts.incrementAndGet();
- tenantOf(application.id()).contact().flatMap(contact -> {
- return ownershipIssues.confirmOwnership(application.ownershipIssueId(),
- summaryOf(application.id()),
- application.issueOwner().orElse(null),
- application.userOwner().orElse(null),
- contact);
- }).ifPresent(newIssueId -> store(newIssueId, application.id()));
- }
- catch (RuntimeException e) { // Catch errors due to wrong data in the controller, or issues client timeout.
- failures.incrementAndGet();
- log.log(Level.INFO, "Exception caught when attempting to file an issue for '" + application.id() + "': " + Exceptions.toMessageString(e));
- }
- });
- return asSuccessFactorDeviation(attempts.get(), failures.get());
- }
-
- private boolean isInCurrentShard(TenantAndApplicationId id) {
- double participants = Math.max(1, controller().curator().cluster().size());
- long ticksSinceEpoch = Math.round((controller().clock().millis() * participants / interval().toMillis()));
- return (ticksSinceEpoch + id.hashCode()) % shards == 0;
- }
-
- private ApplicationSummary summaryOf(TenantAndApplicationId application) {
- var app = applications.requireApplication(application);
- var metrics = new HashMap<DeploymentId, ApplicationSummary.Metric>();
- for (Instance instance : app.instances().values()) {
- for (var kv : instance.deployments().entrySet()) {
- var zone = kv.getKey();
- var deploymentMetrics = kv.getValue().metrics();
- metrics.put(new DeploymentId(instance.id(), zone),
- new ApplicationSummary.Metric(deploymentMetrics.documentCount(),
- deploymentMetrics.queriesPerSecond(),
- deploymentMetrics.writesPerSecond()));
- }
- }
- return new ApplicationSummary(app.id().defaultInstance(), app.activity().lastQueried(), app.activity().lastWritten(),
- app.revisions().last().flatMap(version -> version.buildTime()), metrics);
- }
-
- /** Escalate ownership issues which have not been closed before a defined amount of time has passed. */
- private double ensureConfirmationResponses() {
- AtomicInteger attempts = new AtomicInteger(0);
- AtomicInteger failures = new AtomicInteger(0);
- for (Application application : applications())
- if (isInCurrentShard(application.id()))
- application.ownershipIssueId().ifPresent(issueId -> {
- try {
- attempts.incrementAndGet();
- Tenant tenant = tenantOf(application.id());
- ownershipIssues.ensureResponse(issueId, tenant.contact());
- }
- catch (RuntimeException e) {
- failures.incrementAndGet();
- log.log(Level.INFO, "Exception caught when attempting to escalate issue with id '" + issueId + "': " + Exceptions.toMessageString(e));
- }
- });
- return asSuccessFactorDeviation(attempts.get(), failures.get());
- }
-
- private double updateConfirmedApplicationOwners() {
- AtomicInteger attempts = new AtomicInteger(0);
- AtomicInteger failures = new AtomicInteger(0);
- applications()
- .withProjectId()
- .withProductionDeployment()
- .asList()
- .stream()
- .filter(application -> isInCurrentShard(application.id()))
- .filter(application -> application.ownershipIssueId().isPresent())
- .forEach(application -> {
- attempts.incrementAndGet();
- IssueId issueId = application.ownershipIssueId().get();
- try {
- ownershipIssues.getConfirmedOwner(issueId).ifPresent(owner -> {
- controller().applications().lockApplicationIfPresent(application.id(), lockedApplication ->
- controller().applications().store(lockedApplication.withOwner(owner)));
- });
- }
- catch (RuntimeException e) {
- failures.incrementAndGet();
- log.log(Level.INFO, "Exception caught when attempting to find confirmed owner of issue with id '" + issueId + "': " + Exceptions.toMessageString(e));
- }
- });
- return asSuccessFactorDeviation(attempts.get(), failures.get());
- }
-
- private ApplicationList applications() {
- return ApplicationList.from(controller().applications().readable());
- }
-
- private AccountId determineAssignee(Application application) {
- return application.issueOwner().orElse(null);
- }
-
- private User determineLegacyAssignee(Application application) {
- return application.userOwner().orElse(null);
- }
-
- private Tenant tenantOf(TenantAndApplicationId applicationId) {
- return controller().tenants().get(applicationId.tenant())
- .orElseThrow(() -> new IllegalStateException("No tenant found for application " + applicationId));
- }
-
- protected void store(IssueId issueId, TenantAndApplicationId applicationId) {
- controller().applications().lockApplicationIfPresent(applicationId, application ->
- controller().applications().store(application.withOwnershipIssueId(issueId)));
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java
deleted file mode 100644
index b6f73d6e5e3..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainer.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import ai.vespa.metrics.ControllerMetrics;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.archive.CuratorArchiveBucketDb;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-/**
- * Update archive access permissions with roles from tenants
- *
- * @author andreer
- */
-public class ArchiveAccessMaintainer extends ControllerMaintainer {
-
- private static final String bucketCountMetricName = ControllerMetrics.ARCHIVE_BUCKET_COUNT.baseName();
-
- private final CuratorArchiveBucketDb archiveBucketDb;
- private final ArchiveService archiveService;
- private final ZoneRegistry zoneRegistry;
- private final Metric metric;
-
- public ArchiveAccessMaintainer(Controller controller, Metric metric, Duration interval) {
- super(controller, interval);
- this.archiveBucketDb = controller.archiveBucketDb();
- this.archiveService = controller.serviceRegistry().archiveService();
- this.zoneRegistry = controller().zoneRegistry();
- this.metric = metric;
- }
-
- @Override
- protected double maintain() {
- // Count buckets - so we can alert if we get close to the AWS account limit of 1000
- zoneRegistry.zonesIncludingSystem().all().zones().forEach(z ->
- metric.set(bucketCountMetricName, archiveBucketDb.buckets(z.getVirtualId()).vespaManaged().size(),
- metric.createContext(Map.of(
- "zone", z.getVirtualId().value(),
- "cloud", z.getCloudName().value()))));
-
- zoneRegistry.zonesIncludingSystem().controllerUpgraded().zones().forEach(z -> {
- ZoneId zoneId = z.getVirtualId();
- try {
- var tenantArchiveAccessRoles = cloudTenantArchiveExternalAccessRoles();
- var buckets = archiveBucketDb.buckets(zoneId).vespaManaged();
- archiveService.updatePolicies(zoneId, buckets, tenantArchiveAccessRoles);
- } catch (Exception e) {
- throw new RuntimeException("Failed to maintain archive access in " + zoneId.value(), e);
- }
- });
-
- return 1.0;
- }
-
- private Map<TenantName, ArchiveAccess> cloudTenantArchiveExternalAccessRoles() {
- List<Tenant> tenants = controller().tenants().asList();
- return tenants.stream()
- .filter(t -> t instanceof CloudTenant)
- .map(t -> (CloudTenant) t)
- .collect(Collectors.toUnmodifiableMap(
- Tenant::name, CloudTenant::archiveAccess));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java
deleted file mode 100644
index 8913d6e7166..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdater.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveUriUpdate;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ArchiveUris;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.archive.CuratorArchiveBucketDb;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.stream.Stream;
-
-/**
- * Updates archive URIs for tenants in all zones.
- *
- * @author freva
- */
-public class ArchiveUriUpdater extends ControllerMaintainer {
-
- private static final Set<TenantName> INFRASTRUCTURE_TENANTS = Set.of(SystemApplication.TENANT);
-
- private final ApplicationController applications;
- private final NodeRepository nodeRepository;
- private final CuratorArchiveBucketDb archiveBucketDb;
- private final ZoneRegistry zoneRegistry;
-
- public ArchiveUriUpdater(Controller controller, Duration interval) {
- super(controller, interval);
- this.applications = controller.applications();
- this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- this.archiveBucketDb = controller.archiveBucketDb();
- this.zoneRegistry = controller.zoneRegistry();
- }
-
- @Override
- protected double maintain() {
- Map<ZoneId, Set<TenantName>> tenantsByZone = new HashMap<>();
- Map<ZoneId, Set<CloudAccount>> accountsByZone = new HashMap<>();
-
- controller().zoneRegistry().zonesIncludingSystem().reachable().zones().forEach(zone -> {
- tenantsByZone.put(zone.getVirtualId(), new HashSet<>(INFRASTRUCTURE_TENANTS));
- accountsByZone.put(zone.getVirtualId(), new HashSet<>());
- });
-
- for (var application : applications.asList()) {
- for (var instance : application.instances().values()) {
- for (var deployment : instance.deployments().values()) {
- if (zoneRegistry.isExclave(deployment.cloudAccount())) accountsByZone.get(deployment.zone()).add(deployment.cloudAccount());
- else tenantsByZone.get(deployment.zone()).add(instance.id().tenant());
- }
- }
- }
-
- int failures = 0;
- for (ZoneId zone : tenantsByZone.keySet()) {
- try {
- ArchiveUris zoneArchiveUris = nodeRepository.getArchiveUris(zone);
-
- Stream.of(
- // Tenant URIs that need to be added or updated
- tenantsByZone.get(zone).stream()
- .flatMap(tenant -> archiveBucketDb.archiveUriFor(zone, tenant, true)
- .filter(uri -> !uri.equals(zoneArchiveUris.tenantArchiveUris().get(tenant)))
- .map(uri -> ArchiveUriUpdate.setArchiveUriFor(tenant, uri))
- .stream()),
- // Account URIs that need to be added or updated
- accountsByZone.get(zone).stream()
- .flatMap(account -> archiveBucketDb.archiveUriFor(zone, account, true)
- .filter(uri -> !uri.equals(zoneArchiveUris.accountArchiveUris().get(account)))
- .map(uri -> ArchiveUriUpdate.setArchiveUriFor(account, uri))
- .stream()),
- // Tenant URIs that need to be deleted
- zoneArchiveUris.tenantArchiveUris().keySet().stream()
- .filter(tenant -> !tenantsByZone.get(zone).contains(tenant))
- .map(ArchiveUriUpdate::deleteArchiveUriFor),
- // Account URIs that need to be deleted
- zoneArchiveUris.accountArchiveUris().keySet().stream()
- .filter(account -> !accountsByZone.get(zone).contains(account))
- .map(ArchiveUriUpdate::deleteArchiveUriFor))
- .flatMap(s -> s)
- .forEach(update -> nodeRepository.updateArchiveUri(zone, update));
- } catch (Exception e) {
- log.log(Level.WARNING, "Failed to update archive URI in " + zone + ". Retrying in " + interval() + ". Error: " +
- Exceptions.toMessageString(e));
- failures++;
- }
- }
-
- return asSuccessFactorDeviation(tenantsByZone.size(), failures);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArtifactExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArtifactExpirer.java
deleted file mode 100644
index 02cf7a85445..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ArtifactExpirer.java
+++ /dev/null
@@ -1,130 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.defaults.Defaults;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.artifact.Artifact;
-import com.yahoo.vespa.hosted.controller.api.integration.artifact.ArtifactRegistry;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-import static com.yahoo.yolean.Exceptions.uncheck;
-import static java.util.logging.Level.FINE;
-import static java.util.logging.Level.INFO;
-
-/**
- * Periodically expire unused artifacts, e.g. container images and RPMs. Artifacts with a version that is
- * present in config-models-*.xml are never expired (in cd/publiccd we also consider the model versions in main/public).
- *
- * @author mpolden
- */
-public class ArtifactExpirer extends ControllerMaintainer {
-
- private static final Logger log = Logger.getLogger(ArtifactExpirer.class.getName());
-
- private static final Duration MIN_AGE = Duration.ofDays(14);
-
- private final Path configModelPath;
-
- public ArtifactExpirer(Controller controller, Duration interval) {
- this(controller, interval, Paths.get(Defaults.getDefaults().underVespaHome("conf/configserver-app/")));
- }
-
- public ArtifactExpirer(Controller controller, Duration interval, Path configModelPath) {
- super(controller, interval);
- this.configModelPath = configModelPath;
- }
-
- @Override
- protected double maintain() {
- VersionStatus versionStatus = controller().readVersionStatus();
- return controller().clouds().stream()
- .flatMapToDouble(cloud ->
- controller().serviceRegistry().artifactRegistry(cloud).stream()
- .mapToDouble(artifactRegistry -> maintain(versionStatus, cloud, artifactRegistry)))
- .average()
- .orElse(1);
- }
-
- private double maintain(VersionStatus versionStatus, CloudName cloudName, ArtifactRegistry artifactRegistry) {
- try {
- Instant now = controller().clock().instant();
- List<Artifact> artifactsToExpire = artifactRegistry.list().stream()
- .filter(artifact -> isExpired(artifact, now, versionStatus, modelVersionsInUse()))
- .toList();
- if (!artifactsToExpire.isEmpty()) {
- log.log(INFO, "Expiring " + artifactsToExpire.size() + " artifacts in " + cloudName + ": " + artifactsToExpire);
- artifactRegistry.deleteAll(artifactsToExpire);
- }
- return 0;
- } catch (RuntimeException e) {
- log.log(Level.WARNING, "Failed to expire artifacts in " + cloudName + ". Will retry in " + interval(), e);
- return 1;
- }
- }
-
- /** Returns whether given artifact is expired */
- private boolean isExpired(Artifact artifact, Instant now, VersionStatus versionStatus, Set<Version> versionsInUse) {
- List<VespaVersion> versions = versionStatus.versions();
- versionsInUse.addAll(versions.stream().map(VespaVersion::versionNumber).collect(Collectors.toSet()));
-
- if (versionsInUse.contains(artifact.version())) return false;
- if (versionStatus.isActive(artifact.version())) return false;
- if (artifact.createdAt().isAfter(now.minus(MIN_AGE))) return false;
-
- Version maxVersion = versions.stream().map(VespaVersion::versionNumber).max(Comparator.naturalOrder()).get();
- if (artifact.version().isAfter(maxVersion)) return false; // A future version
-
- return true;
- }
-
- /** Model versions in use in this system, and, if this is a CD system, in the main/public system */
- private Set<Version> modelVersionsInUse() {
- var system = controller().system();
- var versions = versionsForSystem(system);
-
- if (system == SystemName.PublicCd)
- versions.addAll(versionsForSystem(SystemName.Public));
- else if (system == SystemName.cd)
- versions.addAll(versionsForSystem(SystemName.main));
-
- log.log(FINE, "model versions in use: " + versions);
- return versions;
- }
-
- private Set<Version> versionsForSystem(SystemName systemName) {
- var versions = readConfigModelVersionsForSystem(systemName.name().toLowerCase());
- log.log(FINE, "model versions in use in " + systemName.name() + ": " + versions);
- return versions;
- }
-
- private Set<Version> readConfigModelVersionsForSystem(String systemName) {
- List<String> lines = uncheck(() -> Files.readAllLines(configModelPath.resolve("config-models-" + systemName + ".xml")));
- var stringToMatch = "id='VespaModelFactory.";
- return lines.stream()
- .filter(line -> line.contains(stringToMatch))
- .map(line -> {
- var start = line.indexOf(stringToMatch) + stringToMatch.length();
- int end = line.indexOf("'", start);
- return line.substring(start, end);
- })
- .map(Version::fromString)
- .collect(Collectors.toSet());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdater.java
deleted file mode 100644
index 92aaacaa1f0..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdater.java
+++ /dev/null
@@ -1,321 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.application.api.Bcp;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.noderepository.ApplicationPatch;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-
-import java.time.Duration;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.logging.Level;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * This computes, for every application deployment
- * - the current fraction of the application's global traffic it receives.
- * - the max fraction it can possibly receive, given its BCP group membership.
- * - for each cluster in the deployment, average statistics from the other members in the group.
- *
- * These values are sent to a config server of each region where it is consumed by autoscaling.
- *
- * It depends on the traffic metrics collected by DeploymentMetricsMaintainer.
- *
- * @author bratseth
- */
-public class BcpGroupUpdater extends ControllerMaintainer {
-
- private final ApplicationController applications;
- private final NodeRepository nodeRepository;
- private final Double successFactorBaseline;
-
- public BcpGroupUpdater(Controller controller, Duration duration, Double successFactorBaseline) {
- super(controller, duration, successFactorBaseline);
- this.applications = controller.applications();
- this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- this.successFactorBaseline = successFactorBaseline;
- }
-
- public BcpGroupUpdater(Controller controller, Duration duration) {
- this(controller, duration, 1.0);
- }
-
- @Override
- protected double maintain() {
- Exception lastException = null;
- int attempts = 0;
- int failures = 0;
- var metrics = collectClusterMetrics();
- for (var application : applications.asList()) {
- for (var instance : application.instances().values()) {
- for (var deployment : instance.productionDeployments().values()) {
- if (shuttingDown()) return 0.0;
- try {
- attempts++;
- var bcpGroups = BcpGroup.groupsFrom(instance, application.deploymentSpec());
- var patch = new ApplicationPatch();
- addTrafficShare(deployment, bcpGroups, patch);
- addBcpGroupInfo(deployment.zone().region(), metrics.get(instance.id()), bcpGroups, patch);
-
- StringBuilder patchAsStringBuilder = new StringBuilder("Patch of instance ").append(instance.id().serializedForm()).append(": ")
- .append("\n\tcurrentReadShare: ")
- .append(patch.currentReadShare)
- .append("\n\tmaxReadShare: ")
- .append(patch.maxReadShare);
- for (Map.Entry<String, ApplicationPatch.ClusterPatch> entry : patch.clusters.entrySet()) {
- String key = entry.getKey();
- ApplicationPatch.ClusterPatch value = entry.getValue();
- patchAsStringBuilder.append("\n\tbcpGroupInfo for ").append(key).append(": ")
- .append("\n\t\tcpuCostPerQuery: ")
- .append(value.bcpGroupInfo.cpuCostPerQuery)
- .append("\n\t\tqueryRate: ")
- .append(value.bcpGroupInfo.queryRate)
- .append("\n\t\tgrowthRateHeadroom: ")
- .append(value.bcpGroupInfo.growthRateHeadroom);
- }
- log.log(Level.FINER, patchAsStringBuilder.toString());
- nodeRepository.patchApplication(deployment.zone(), instance.id(), patch);
- }
- catch (Exception e) {
- // Some failures due to locked applications are expected and benign
- failures++;
- lastException = e;
- }
- }
- }
- }
- double successFactorDeviation = asSuccessFactorDeviation(attempts, failures);
- if ( successFactorDeviation == -successFactorBaseline )
- log.log(Level.WARNING, "Could not update traffic share on any applications", lastException);
- else if ( successFactorDeviation < 0 )
- log.log(Level.FINE, "Could not update traffic share on all applications", lastException);
- return successFactorDeviation;
- }
-
- /** Adds deployment traffic share to the given patch. */
- private void addTrafficShare(Deployment deployment, List<BcpGroup> bcpGroups, ApplicationPatch patch) {
- // maxReadShare / currentReadShare = how much additional traffic must the zone be able to handle
- double currentReadShare = 0; // How much of the total traffic of the group(s) this is a member of does this deployment receive
- double maxReadShare = 0; // How much of the total traffic of the group(s) this is a member of might this deployment receive if a member of the group fails
- for (BcpGroup group : bcpGroups) {
- if ( ! group.contains(deployment.zone().region())) continue;
-
- double deploymentQps = deployment.metrics().queriesPerSecond();
- double groupQps = group.totalQps();
- double fraction = group.fraction(deployment.zone().region());
- currentReadShare += groupQps == 0 ? 0 : fraction * deploymentQps / groupQps;
- maxReadShare += group.size() == 1
- ? currentReadShare
- : groupQps != 0
- ? fraction * (deploymentQps + group.maxQpsExcluding(deployment.zone().region()) / (group.size() - 1)) / groupQps
- : 0;
- }
- patch.currentReadShare = currentReadShare;
- patch.maxReadShare = maxReadShare;
- }
-
- private Map<ApplicationId, Map<ClusterSpec.Id, ClusterDeploymentMetrics>> collectClusterMetrics() {
- Map<ApplicationId, Map<ClusterSpec.Id, ClusterDeploymentMetrics>> metrics = new HashMap<>();
- for (var deploymentEntry : new HashMap<>(controller().applications().deploymentInfo()).entrySet()) {
- if ( ! deploymentEntry.getKey().zoneId().environment().isProduction()) continue;
- var appEntry = metrics.computeIfAbsent(deploymentEntry.getKey().applicationId(), __ -> new HashMap<>());
- for (var clusterEntry : deploymentEntry.getValue().clusters().entrySet()) {
- var clusterMetrics = appEntry.computeIfAbsent(clusterEntry.getKey(), __ -> new ClusterDeploymentMetrics());
- clusterMetrics.put(deploymentEntry.getKey().zoneId().region(),
- new DeploymentMetrics(clusterEntry.getValue().target().metrics().queryRate(),
- clusterEntry.getValue().target().metrics().growthRateHeadroom(),
- clusterEntry.getValue().target().metrics().cpuCostPerQuery()));
- }
- }
- return metrics;
- }
-
- /** Adds bcp group info to the given patch, for any clusters where we have information. */
- private void addBcpGroupInfo(RegionName regionToUpdate, Map<ClusterSpec.Id, ClusterDeploymentMetrics> metrics,
- List<BcpGroup> bcpGroups, ApplicationPatch patch) {
- if (metrics == null) return;
- for (var clusterEntry : metrics.entrySet()) {
- addClusterBcpGroupInfo(clusterEntry.getKey(), clusterEntry.getValue(), regionToUpdate, bcpGroups, patch);
- }
- }
-
- private void addClusterBcpGroupInfo(ClusterSpec.Id id, ClusterDeploymentMetrics metrics,
- RegionName regionToUpdate, List<BcpGroup> bcpGroups, ApplicationPatch patch) {
- var weightedSumOfMaxMetrics = DeploymentMetrics.empty();
- double sumOfCompleteMemberships = 0;
- for (BcpGroup bcpGroup : bcpGroups) {
- if ( ! bcpGroup.contains(regionToUpdate)) continue;
- var groupMetrics = metrics.subsetOf(bcpGroup);
- if ( ! groupMetrics.isCompleteExcluding(regionToUpdate, bcpGroup)) continue;
- var max = groupMetrics.maxQueryRateExcluding(regionToUpdate, bcpGroup);
- if (max.isEmpty()) continue;
-
- weightedSumOfMaxMetrics = weightedSumOfMaxMetrics.add(max.get().multipliedBy(bcpGroup.fraction(regionToUpdate)));
- sumOfCompleteMemberships += bcpGroup.fraction(regionToUpdate);
- }
- if (sumOfCompleteMemberships > 0)
- patch.clusters.put(id.value(), weightedSumOfMaxMetrics.dividedBy(sumOfCompleteMemberships).asClusterPatch());
- }
-
- /**
- * A set of regions which will take over traffic from each other if one of them fails.
- * Each region will take an equal share (modulated by fraction) of the failing region's traffic.
- *
- * A regions membership in a group may be partial, represented by a fraction [0, 1],
- * in which case the other regions will collectively only take that fraction of the failing regions traffic,
- * and symmetrically, the region will only take its fraction of its share of traffic of any other failing region.
- */
- private static class BcpGroup {
-
- /** The instance which has this group. */
- private final Instance instance;
-
- /** Regions in this group, with their fractions. */
- private final Map<RegionName, Double> regions;
-
- /** Creates a group of a subset of the deployments in this instance. */
- private BcpGroup(Instance instance, Map<RegionName, Double> regions) {
- this.instance = instance;
- this.regions = regions;
- }
-
- /** Returns the sum of the fractional memberships of this. */
- double size() {
- return regions.values().stream().mapToDouble(f -> f).sum();
- }
-
- Set<RegionName> regions() { return regions.keySet(); }
-
- double fraction(RegionName region) {
- return regions.getOrDefault(region, 0.0);
- }
-
- boolean contains(RegionName region) {
- return regions.containsKey(region);
- }
-
- double totalQps() {
- return instance.productionDeployments().values().stream()
- .mapToDouble(i -> i.metrics().queriesPerSecond()).sum();
- }
-
- double maxQpsExcluding(RegionName region) {
- return instance.productionDeployments().values().stream()
- .filter(d -> ! d.zone().region().equals(region))
- .mapToDouble(d -> d.metrics().queriesPerSecond() * fraction(d.zone().region()))
- .max()
- .orElse(0);
- }
-
- private static Bcp bcpOf(InstanceName instanceName, DeploymentSpec deploymentSpec) {
- var instanceSpec = deploymentSpec.instance(instanceName);
- if (instanceSpec.isEmpty()) return Bcp.empty();
- return instanceSpec.get().bcp();
- }
-
- private static Map<RegionName, Double> regionsFrom(Instance instance) {
- return instance.productionDeployments().values().stream()
- .collect(Collectors.toMap(deployment -> deployment.zone().region(), __ -> 1.0));
- }
-
- private static Map<RegionName, Double> regionsFrom(Bcp.Group groupSpec) {
- return groupSpec.members().stream()
- .collect(Collectors.toMap(member -> member.region(), member -> member.fraction()));
- }
-
- static List<BcpGroup> groupsFrom(Instance instance, DeploymentSpec deploymentSpec) {
- Bcp bcp = bcpOf(instance.name(), deploymentSpec);
- if (bcp.isEmpty())
- return List.of(new BcpGroup(instance, regionsFrom(instance)));
- return bcp.groups().stream().map(groupSpec -> new BcpGroup(instance, regionsFrom(groupSpec))).toList();
- }
-
- }
-
- record ApplicationClusterKey(ApplicationId application, ClusterSpec.Id cluster) { }
-
- static class ClusterDeploymentMetrics {
-
- private final Map<RegionName, DeploymentMetrics> deploymentMetrics;
-
- public ClusterDeploymentMetrics() {
- this.deploymentMetrics = new ConcurrentHashMap<>();
- }
-
- public ClusterDeploymentMetrics(Map<RegionName, DeploymentMetrics> deploymentMetrics) {
- this.deploymentMetrics = new ConcurrentHashMap<>(deploymentMetrics);
- }
-
- void put(RegionName region, DeploymentMetrics metrics) {
- deploymentMetrics.put(region, metrics);
- }
-
- ClusterDeploymentMetrics subsetOf(BcpGroup group) {
- Map<RegionName, DeploymentMetrics> filteredMetrics = new HashMap<>();
- for (var entry : deploymentMetrics.entrySet()) {
- if (group.contains(entry.getKey()))
- filteredMetrics.put(entry.getKey(), entry.getValue());
- }
- return new ClusterDeploymentMetrics(filteredMetrics);
- }
-
- /** Returns whether this has deployment metrics for each of the deployments in the given instance. */
- boolean isCompleteExcluding(RegionName regionToExclude, BcpGroup bcpGroup) {
- return regionsExcluding(regionToExclude, bcpGroup).allMatch(region -> deploymentMetrics.containsKey(region));
- }
-
- /** Returns the metrics with the max query rate among the given instance, if any. */
- Optional<DeploymentMetrics> maxQueryRateExcluding(RegionName regionToExclude, BcpGroup bcpGroup) {
- return regionsExcluding(regionToExclude, bcpGroup)
- .map(region -> deploymentMetrics.get(region))
- .max(Comparator.comparingDouble(m -> m.queryRate));
- }
-
- private Stream<RegionName> regionsExcluding(RegionName regionToExclude, BcpGroup bcpGroup) {
- return bcpGroup.regions().stream()
- .filter(region -> ! region.equals(regionToExclude));
- }
-
- }
-
- /** Metrics for a given application, cluster and deployment. */
- record DeploymentMetrics(double queryRate, double growthRateHeadroom, double cpuCostPerQuery) {
-
- public ApplicationPatch.ClusterPatch asClusterPatch() {
- return new ApplicationPatch.ClusterPatch(new ApplicationPatch.BcpGroupInfo(queryRate, growthRateHeadroom, cpuCostPerQuery));
- }
-
- DeploymentMetrics dividedBy(double d) {
- return new DeploymentMetrics(queryRate / d, growthRateHeadroom / d, cpuCostPerQuery / d);
- }
-
- DeploymentMetrics multipliedBy(double m) {
- return new DeploymentMetrics(queryRate * m, growthRateHeadroom * m, cpuCostPerQuery * m);
- }
-
- DeploymentMetrics add(DeploymentMetrics other) {
- return new DeploymentMetrics(queryRate + other.queryRate,
- growthRateHeadroom + other.growthRateHeadroom,
- cpuCostPerQuery + other.cpuCostPerQuery);
- }
-
- public static DeploymentMetrics empty() { return new DeploymentMetrics(0, 0, 0); }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingDatabaseMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingDatabaseMaintainer.java
deleted file mode 100644
index 426abb16549..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingDatabaseMaintainer.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.Controller;
-
-import java.time.Duration;
-import java.util.EnumSet;
-
-/**
- * @author olaa
- */
-public class BillingDatabaseMaintainer extends ControllerMaintainer {
-
- public BillingDatabaseMaintainer(Controller controller, Duration interval) {
- super(controller, interval, null, EnumSet.of(SystemName.PublicCd));
- }
-
- @Override
- protected double maintain() {
- controller().serviceRegistry().billingDatabase().maintain();
- return 0.0;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainer.java
deleted file mode 100644
index 7434fce31bf..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainer.java
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillStatus;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClient;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingReporter;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.InvoiceUpdate;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-public class BillingReportMaintainer extends ControllerMaintainer {
-
- private final BillingReporter reporter;
- private final BillingController billing;
- private final BillingDatabaseClient databaseClient;
-
- private final PlanRegistry plans;
-
- public BillingReportMaintainer(Controller controller, Duration interval) {
- super(controller, interval, null, Set.of(SystemName.Public, SystemName.PublicCd));
- reporter = controller.serviceRegistry().billingReporter();
- billing = controller.serviceRegistry().billingController();
- databaseClient = controller.serviceRegistry().billingDatabase();
- plans = controller.serviceRegistry().planRegistry();
- }
-
- @Override
- protected double maintain() {
- maintainTenants();
-
- var updates = maintainInvoices();
- log.fine("Updated invoices: " + updates);
-
- return 0.0;
- }
-
- private void maintainTenants() {
- var tenants = cloudTenants();
- var tenantNames = List.copyOf(tenants.keySet());
- var billableTenants = billableTenants(tenantNames);
-
- billableTenants.forEach(tenant -> {
- controller().tenants().lockIfPresent(tenant, LockedTenant.Cloud.class, locked -> {
- var ref = reporter.maintainTenant(locked.get());
- if (locked.get().billingReference().isEmpty() || ! locked.get().billingReference().get().equals(ref)) {
- controller().tenants().store(locked.with(ref));
- }
- });
- });
- }
-
- List<InvoiceUpdate> maintainInvoices() {
- var updates = new ArrayList<InvoiceUpdate>();
-
- var tenants = cloudTenants();
- var billsNeedingMaintenance = databaseClient.readBills().stream()
- .filter(bill -> bill.getExportedId().isPresent())
- .filter(exported -> ! exported.status().isFinal())
- .toList();
-
- for (var bill : billsNeedingMaintenance) {
- var exportedId = bill.getExportedId().orElseThrow();
- var update = reporter.maintainInvoice(tenants.get(bill.tenant()), bill);
- switch (update.type()) {
- case UNMODIFIED -> log.finer(() ->invoiceMessage(bill.id(), exportedId) + " was not modified");
- case MODIFIED -> log.fine(invoiceMessage(bill.id(), exportedId) + " was updated with " + update.itemsUpdate().get());
- case UNMODIFIABLE -> {
- // This check is needed to avoid setting the status multiple times
- if (bill.status() != BillStatus.FROZEN) {
- log.fine(() -> invoiceMessage(bill.id(), exportedId) + " is now unmodifiable");
- databaseClient.setStatus(bill.id(), "system", BillStatus.FROZEN);
- }
- }
- case REMOVED -> {
- log.fine(() -> invoiceMessage(bill.id(), exportedId) + " has been deleted in the external system");
- // Reset the exportedId to null, so that we don't maintain it again
- databaseClient.setExportedInvoiceId(bill.id(), null);
- }
- case PAID -> {
- log.fine(() -> invoiceMessage(bill.id(), exportedId) + " has been paid in the external system");
- databaseClient.setStatus(bill.id(), "system", BillStatus.SUCCESSFUL);
- }
- case VOIDED -> {
- log.fine(() -> invoiceMessage(bill.id(), exportedId) + " has been voided in the external system");
- databaseClient.setStatus(bill.id(), "system", BillStatus.VOID);
- }
- }
- updates.add(update);
- }
- return updates;
- }
-
- private String invoiceMessage(Bill.Id billId, String invoiceId) {
- return "Invoice '" + invoiceId + "' for bill '" + billId.value() + "'";
- }
-
- private Map<TenantName, CloudTenant> cloudTenants() {
- return controller().tenants().asList()
- .stream()
- .filter(CloudTenant.class::isInstance)
- .map(CloudTenant.class::cast)
- .collect(Collectors.toMap(
- Tenant::name,
- Function.identity()));
- }
-
- private List<Plan> billablePlans() {
- return plans.all().stream()
- .filter(Plan::isBilled)
- .toList();
- }
-
- private List<TenantName> billableTenants(List<TenantName> tenants) {
- return billablePlans().stream()
- .flatMap(p -> billing.tenantsWithPlan(tenants, p.id()).stream())
- .toList();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CertificatePoolMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CertificatePoolMaintainer.java
deleted file mode 100644
index 5e6e495e473..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CertificatePoolMaintainer.java
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import ai.vespa.metrics.ControllerMetrics;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.secretstore.SecretNotFoundException;
-import com.yahoo.container.jdisc.secretstore.SecretStore;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.flags.BooleanFlag;
-import com.yahoo.vespa.flags.IntFlag;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.flags.StringFlag;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider;
-import com.yahoo.vespa.hosted.controller.application.GeneratedEndpoint;
-import com.yahoo.vespa.hosted.controller.certificate.AssignedCertificate;
-import com.yahoo.vespa.hosted.controller.certificate.UnassignedCertificate;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * Manages a pool of ready-to-use endpoint certificates.
- *
- * @author andreer
- */
-public class CertificatePoolMaintainer extends ControllerMaintainer {
-
- private static final Logger log = Logger.getLogger(CertificatePoolMaintainer.class.getName());
-
- private final CuratorDb curator;
- private final SecretStore secretStore;
- private final EndpointCertificateProvider endpointCertificateProvider;
- private final Metric metric;
- private final Controller controller;
- private final IntFlag certPoolSize;
- private final StringFlag endpointCertificateAlgo;
- private final BooleanFlag useAlternateCertProvider;
-
- public CertificatePoolMaintainer(Controller controller, Metric metric, Duration interval) {
- super(controller, interval);
- this.controller = controller;
- this.secretStore = controller.secretStore();
- this.certPoolSize = PermanentFlags.CERT_POOL_SIZE.bindTo(controller.flagSource());
- this.useAlternateCertProvider = PermanentFlags.USE_ALTERNATIVE_ENDPOINT_CERTIFICATE_PROVIDER.bindTo(controller.flagSource());
- this.endpointCertificateAlgo = PermanentFlags.ENDPOINT_CERTIFICATE_ALGORITHM.bindTo(controller.flagSource());
- this.curator = controller.curator();
- this.endpointCertificateProvider = controller.serviceRegistry().endpointCertificateProvider();
- this.metric = metric;
- }
-
- protected double maintain() {
- try {
- moveRequestedCertsToReady();
- List<UnassignedCertificate> certificatePool = curator.readUnassignedCertificates();
-
- // Create metric for available certificates in the pool as a fraction of configured size
- int poolSize = certPoolSize.value();
- long available = certificatePool.stream().filter(c -> c.state() == UnassignedCertificate.State.ready).count();
- metric.set(ControllerMetrics.CERTIFICATE_POOL_AVAILABLE.baseName(), (poolSize > 0 ? ((double)available/poolSize) : 1.0), metric.createContext(Map.of()));
-
- if (certificatePool.size() < poolSize) {
- provisionCertificate();
- }
- } catch (Exception e) {
- log.log(Level.SEVERE, "Failed to maintain certificate pool", e);
- return 1.0;
- }
- return 0.0;
- }
-
- private void moveRequestedCertsToReady() {
- try (Mutex lock = controller.curator().lockCertificatePool()) {
- for (UnassignedCertificate cert : curator.readUnassignedCertificates()) {
- if (cert.state() == UnassignedCertificate.State.ready) continue;
- try {
- OptionalInt maxKeyVersion = secretStore.listSecretVersions(cert.certificate().keyName()).stream().mapToInt(i -> i).max();
- OptionalInt maxCertVersion = secretStore.listSecretVersions(cert.certificate().certName()).stream().mapToInt(i -> i).max();
- if (maxKeyVersion.isPresent() && maxCertVersion.equals(maxKeyVersion)) {
- curator.writeUnassignedCertificate(cert.withState(UnassignedCertificate.State.ready));
- log.log(Level.INFO, "Readied certificate %s".formatted(cert.id()));
- }
- } catch (SecretNotFoundException s) {
- // Likely because the certificate is very recently provisioned - ignore till next time - should we log?
- log.log(Level.INFO, "Cannot ready certificate %s yet, will retry in %s".formatted(cert.id(), interval()));
- }
- }
- }
- }
-
- private void provisionCertificate() {
- try (Mutex lock = controller.curator().lockCertificatePool()) {
- Set<String> existingNames = controller.curator().readUnassignedCertificates().stream().map(UnassignedCertificate::id).collect(Collectors.toSet());
-
- curator.readAssignedCertificates().stream()
- .map(AssignedCertificate::certificate)
- .map(EndpointCertificate::generatedId)
- .forEach(id -> id.ifPresent(existingNames::add));
-
- String id = generateId();
- while (existingNames.contains(id)) id = generateId();
- List<String> dnsNames = wildcardDnsNames(id);
- EndpointCertificate cert = endpointCertificateProvider.requestCaSignedCertificate(
- "preprovisioned.%s".formatted(id),
- dnsNames,
- Optional.empty(),
- endpointCertificateAlgo.value(),
- useAlternateCertProvider.value()).withGeneratedId(id);
-
- UnassignedCertificate certificate = new UnassignedCertificate(cert, UnassignedCertificate.State.requested);
- curator.writeUnassignedCertificate(certificate);
- }
- }
-
- private List<String> wildcardDnsNames(String id) {
- DeploymentId defaultDeployment = new DeploymentId(ApplicationId.defaultId(), ZoneId.defaultId());
- return controller.routing().certificateDnsNames(defaultDeployment, // Not used for non-legacy names
- DeploymentSpec.empty, // Not used for non-legacy names
- id,
- false);
- }
-
- private String generateId() {
- return GeneratedEndpoint.createPart(controller.random(true));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessor.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessor.java
deleted file mode 100644
index 51720806371..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessor.java
+++ /dev/null
@@ -1,267 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-/**
- * @author smorgrav
- */
-public class ChangeManagementAssessor {
-
- private final NodeRepository nodeRepository;
-
- public ChangeManagementAssessor(NodeRepository nodeRepository) {
- this.nodeRepository = nodeRepository;
- }
-
- public Assessment assessment(List<String> impactedHostnames, ZoneId zone) {
- return assessmentInner(impactedHostnames, nodeRepository.list(zone, NodeFilter.all()), zone);
- }
-
- Assessment assessmentInner(List<String> impactedHostnames, List<Node> allNodes, ZoneId zone) {
- List<String> impactedParentHosts = toParentHosts(impactedHostnames, allNodes);
- // Group impacted application nodes by parent host
- Map<Node, List<Node>> prParentHost = allNodes.stream()
- .filter(node -> node.state() == Node.State.active) //TODO look at more states?
- .filter(node -> impactedParentHosts.contains(node.parentHostname().map(HostName::value).orElse("")))
- .collect(Collectors.groupingBy(node ->
- allNodes.stream()
- .filter(parent -> parent.hostname().equals(node.parentHostname().get()))
- .findFirst().orElseThrow()
- ));
-
- // Group nodes pr cluster
- Map<Cluster, List<Node>> prCluster = prParentHost.values()
- .stream()
- .flatMap(Collection::stream)
- .collect(Collectors.groupingBy(ChangeManagementAssessor::clusterKey));
-
- var tenantHosts = prParentHost.keySet().stream()
- .filter(node -> node.type() == NodeType.host)
- .map(node -> node.hostname())
- .toList();
-
- boolean allHostsReplacable = tenantHosts.isEmpty() || nodeRepository.isReplaceable(zone, tenantHosts);
-
- // Report assessment pr cluster
- var clusterAssessments = prCluster.entrySet().stream().map((entry) -> {
- Cluster cluster = entry.getKey();
- List<Node> nodes = entry.getValue();
-
- long[] totalStats = clusterStats(cluster, allNodes);
- long[] impactedStats = clusterStats(cluster, nodes);
-
- ClusterAssessment assessment = new ClusterAssessment();
- assessment.app = cluster.getApp();
- assessment.zone = zone.value();
- assessment.cluster = cluster.getClusterType() + ":" + cluster.getClusterId();
- assessment.clusterSize = totalStats[0];
- assessment.clusterImpact = impactedStats[0];
- assessment.groupsTotal = totalStats[1];
- assessment.groupsImpact = impactedStats[1];
-
-
- // TODO check upgrade policy
- assessment.upgradePolicy = "na";
- // TODO do some heuristic on suggestion action
- assessment.suggestedAction = allHostsReplacable ? "Retire all hosts" : "nothing";
- // TODO do some heuristic on impact
- assessment.impact = getImpact(cluster, impactedStats, totalStats);
-
- return assessment;
- }).toList();
-
- var hostAssessments = prParentHost.entrySet().stream().map((entry) -> {
- HostAssessment hostAssessment = new HostAssessment();
- hostAssessment.hostName = entry.getKey().hostname().value();
- hostAssessment.switchName = entry.getKey().switchHostname().orElse(null);
- hostAssessment.numberOfChildren = entry.getValue().size();
-
- //TODO: Some better heuristic for what's considered problematic
- hostAssessment.numberOfProblematicChildren = (int) entry.getValue().stream()
- .mapToInt(node -> prCluster.get(clusterKey(node)).size())
- .filter(i -> i > 1)
- .count();
-
- return hostAssessment;
- }).toList();
-
- return new Assessment(clusterAssessments, hostAssessments);
- }
-
- private List<String> toParentHosts(List<String> impactedHostnames, List<Node> allNodes) {
- return impactedHostnames.stream()
- .flatMap(hostname ->
- allNodes.stream()
- .filter(node -> List.of(NodeType.config, NodeType.proxy, NodeType.host).contains(node.type()))
- .filter(node -> hostname.equals(node.hostname().value()) || hostname.equals(node.parentHostname().map(HostName::value).orElse("")))
- .map(node -> {
- if (node.type() == NodeType.host)
- return node.hostname().value();
- return node.parentHostname().get().value();
- }).findFirst().stream()
- )
- .toList();
- }
-
- private static Cluster clusterKey(Node node) {
- if (node.owner().isEmpty())
- return Cluster.EMPTY;
- String appId = node.owner().get().serializedForm();
- return new Cluster(node.clusterType(), node.clusterId(), appId, node.type());
- }
-
- private static long[] clusterStats(Cluster cluster, List<Node> containerNodes) {
- List<Node> clusterNodes = containerNodes.stream().filter(node -> cluster.equals(clusterKey(node))).toList();
- long groups = clusterNodes.stream().map(Node::group).distinct().count();
- return new long[] { clusterNodes.size(), groups};
- }
-
- private String getImpact(Cluster cluster, long[] impactedStats, long[] totalStats) {
- switch (cluster.getNodeType()) {
- case tenant:
- return getTenantImpact(cluster, impactedStats, totalStats);
- case proxy:
- return getProxyImpact(impactedStats[0], totalStats[0]);
- case config:
- return getConfigServerImpact(impactedStats[0]);
- default:
- return "Unkown impact";
- }
- }
-
- private String getTenantImpact(Cluster cluster, long[] impactedStats, long[] totalStats) {
- switch (cluster.getClusterType()) {
- case container:
- return getContainerImpact(impactedStats[0], totalStats[0]);
- case content:
- case combined:
- return getContentImpact(totalStats[1] > 1, impactedStats[0], impactedStats[1]);
- default:
- return "Unknown impact";
- }
- }
-
- private String getProxyImpact(long impactedNodes, long totalNodes) {
- int impact = (int) (100.0 * impactedNodes / totalNodes);
- return impact + "% of routing nodes impacted. Consider reprovisioning if too many";
- }
-
- private String getConfigServerImpact(long impactedNodes) {
- if (impactedNodes == 1) {
- return "Acceptable impact";
- }
- return "Large impact. Consider reprovisioning one or more config servers";
- }
-
- private String getContainerImpact(long impactedNodes, long totalNodes) {
- if ((double) impactedNodes / totalNodes <= 0.1) {
- return "Impact not larger than upgrade policy";
- }
- return "Impact larger than upgrade policy";
- }
-
- private String getContentImpact(boolean isGrouped, long impactedNodes, long impactedGroups) {
- if ((isGrouped && impactedGroups == 1) || impactedNodes == 1)
- return "Impact not larger than upgrade policy";
- return "Impact larger than upgrade policy";
- }
-
-
- public static class Assessment {
- List<ClusterAssessment> clusterAssessments;
- List<HostAssessment> hostAssessments;
-
- Assessment(List<ClusterAssessment> clusterAssessments, List<HostAssessment> hostAssessments) {
- this.clusterAssessments = clusterAssessments;
- this.hostAssessments = hostAssessments;
- }
-
- public List<ClusterAssessment> getClusterAssessments() {
- return clusterAssessments;
- }
-
- public List<HostAssessment> getHostAssessments() {
- return hostAssessments;
- }
- }
-
- public static class ClusterAssessment {
- public String app;
- public String zone;
- public String cluster;
- public long clusterImpact;
- public long clusterSize;
- public long groupsImpact;
- public long groupsTotal;
- public String upgradePolicy;
- public String suggestedAction;
- public String impact;
- }
-
- public static class HostAssessment {
- public String hostName;
- public String switchName;
- public int numberOfChildren;
- public int numberOfProblematicChildren;
- }
-
- private static class Cluster {
- private Node.ClusterType clusterType;
- private String clusterId;
- private String app;
- private NodeType nodeType;
-
- public final static Cluster EMPTY = new Cluster(Node.ClusterType.unknown, "na", "na", NodeType.tenant);
-
- public Cluster(Node.ClusterType clusterType, String clusterId, String app, NodeType nodeType) {
- this.clusterType = clusterType;
- this.clusterId = clusterId;
- this.app = app;
- this.nodeType = nodeType;
- }
-
- public Node.ClusterType getClusterType() {
- return clusterType;
- }
-
- public String getClusterId() {
- return clusterId;
- }
-
- public String getApp() {
- return app;
- }
-
- public NodeType getNodeType() {
- return nodeType;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Cluster cluster = (Cluster) o;
- return Objects.equals(clusterType, cluster.clusterType) &&
- Objects.equals(clusterId, cluster.clusterId) &&
- Objects.equals(app, cluster.app);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(clusterType, clusterId, app);
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java
deleted file mode 100644
index 9f687249f38..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainer.java
+++ /dev/null
@@ -1,136 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-
-import java.time.Duration;
-import java.time.ZonedDateTime;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * @author olaa
- */
-public class ChangeRequestMaintainer extends ControllerMaintainer {
-
- private final Logger logger = Logger.getLogger(ChangeRequestMaintainer.class.getName());
- private final ChangeRequestClient changeRequestClient;
- private final CuratorDb curator;
- private final NodeRepository nodeRepository;
- private final SystemName system;
-
- public ChangeRequestMaintainer(Controller controller, Duration interval) {
- super(controller, interval, null, SystemName.allOf(Predicate.not(SystemName::isPublic)));
- this.changeRequestClient = controller.serviceRegistry().changeRequestClient();
- this.curator = controller.curator();
- this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- this.system = controller.system();
- }
-
-
- @Override
- protected double maintain() {
- var currentChangeRequests = pruneOldChangeRequests();
- var changeRequests = changeRequestClient.getChangeRequests(currentChangeRequests);
-
- logger.fine(() -> "Found requests: " + changeRequests);
- storeChangeRequests(changeRequests);
-
- return 1.0;
- }
-
- private void storeChangeRequests(List<ChangeRequest> changeRequests) {
- var existingChangeRequests = curator.readChangeRequests()
- .stream()
- .collect(Collectors.toMap(ChangeRequest::getId, Function.identity()));
-
- var hostsByZone = hostsByZone();
- // Create or update requests in curator
- try (var lock = curator.lockChangeRequests()) {
- changeRequests.forEach(changeRequest -> {
- var optionalZone = inferZone(changeRequest, hostsByZone);
- optionalZone.ifPresentOrElse(zone -> {
- var vcmr = existingChangeRequests
- .getOrDefault(changeRequest.getId(), new VespaChangeRequest(changeRequest, zone))
- .withSource(changeRequest.getChangeRequestSource())
- .withImpact(changeRequest.getImpact())
- .withApproval(changeRequest.getApproval());
- logger.fine(() -> "Storing " + vcmr);
- curator.writeChangeRequest(vcmr);
- },
- () -> approveChangeRequest(changeRequest));
- });
- }
- }
-
- // Deletes closed change requests older than 7 days, returns the current list of requests
- private List<ChangeRequest> pruneOldChangeRequests() {
- List<ChangeRequest> currentChangeRequests = new ArrayList<>();
-
- try (var lock = curator.lockChangeRequests()) {
- for (var changeRequest : curator.readChangeRequests()) {
- if (shouldDeleteChangeRequest(changeRequest.getChangeRequestSource())) {
- curator.deleteChangeRequest(changeRequest);
- } else {
- currentChangeRequests.add(changeRequest);
- }
- }
- }
- return currentChangeRequests;
- }
-
- private Map<ZoneId, List<String>> hostsByZone() {
- return controller().zoneRegistry()
- .zones()
- .reachable()
- .in(Environment.prod)
- .ids()
- .stream()
- .collect(Collectors.toMap(
- zone -> zone,
- zone -> nodeRepository.list(zone, NodeFilter.all())
- .stream()
- .map(node -> node.hostname().value())
- .toList()
- ));
- }
-
- private Optional<ZoneId> inferZone(ChangeRequest changeRequest, Map<ZoneId, List<String>> hostsByZone) {
- return hostsByZone.entrySet().stream()
- .filter(entry -> !Collections.disjoint(entry.getValue(), changeRequest.getImpactedHosts()))
- .map(Map.Entry::getKey)
- .findFirst();
- }
-
- private boolean shouldDeleteChangeRequest(ChangeRequestSource source) {
- return source.isClosed() &&
- source.plannedStartTime()
- .plus(Duration.ofDays(7))
- .isBefore(ZonedDateTime.now());
- }
-
- private void approveChangeRequest(ChangeRequest changeRequest) {
- if (system.equals(SystemName.main) &&
- changeRequest.getApproval() == ChangeRequest.Approval.REQUESTED) {
- logger.info("Approving " + changeRequest.getChangeRequestSource().id());
- changeRequestClient.approveChangeRequest(changeRequest);
- }
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudAccountVerifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudAccountVerifier.java
deleted file mode 100644
index fedfea792f3..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudAccountVerifier.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.tenant.CloudAccountInfo;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Set;
-import java.util.logging.Logger;
-
-import static java.util.logging.Level.WARNING;
-
-/**
- * Verifies the cloud accounts that may be used by a given user have applied the enclave template
- * and extracts the version of the applied template.
- *
- * All maintainers that operate on external cloud accounts should use the list on the Tenant instance
- * maintained by this class rather than the cloud-accounts feature flag.
- *
- * The template version can be used to determine if new features can be enabled for the cloud account.
- *
- * @author freva
- */
-public class CloudAccountVerifier extends ControllerMaintainer {
-
- private static final Logger logger = Logger.getLogger(CloudAccountVerifier.class.getName());
-
- CloudAccountVerifier(Controller controller, Duration interval) {
- super(controller, interval, null, Set.of(SystemName.PublicCd, SystemName.Public));
- }
-
- @Override
- protected double maintain() {
- int attempts = 0, failures = 0;
- for (Tenant tenant : controller().tenants().asList()) {
- try {
- attempts++;
- List<CloudAccountInfo> cloudAccountInfos = controller().applications().accountsOf(tenant.name()).stream()
- .flatMap(account -> controller().serviceRegistry()
- .archiveService()
- .getEnclaveTemplateVersion(account)
- .map(version -> new CloudAccountInfo(account, version))
- .stream())
- .toList();
- controller().tenants().updateCloudAccounts(tenant.name(), cloudAccountInfos);
- } catch (RuntimeException e) {
- logger.log(WARNING, "Failed to verify cloud accounts for tenant " + tenant.name(), e);
- failures++;
- }
- }
- return asSuccessFactorDeviation(attempts, failures);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudDatabaseMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudDatabaseMaintainer.java
deleted file mode 100644
index 73204fb1655..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudDatabaseMaintainer.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-
-public class CloudDatabaseMaintainer extends ControllerMaintainer {
-
- public CloudDatabaseMaintainer(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- protected double maintain() {
- try {
- var tenants = controller().tenants().asList().stream().map(Tenant::name).toList();
- controller().serviceRegistry().billingController().updateCache(tenants);
- } catch (Exception e) {
- log.warning("Could not update cloud database cache: " + Exceptions.toMessageString(e));
- return 1.0;
- }
- return 0.0;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
deleted file mode 100644
index 1e261f78db3..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirer.java
+++ /dev/null
@@ -1,273 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.flags.BooleanFlag;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.ListFlag;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.notification.MailTemplating;
-import com.yahoo.vespa.hosted.controller.notification.Notification;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import com.yahoo.vespa.hosted.controller.persistence.TrialNotifications;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Predicate;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.EXPIRED;
-import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.EXPIRES_IMMEDIATELY;
-import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.MID_CHECK_IN;
-import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.SIGNED_UP;
-import static com.yahoo.vespa.hosted.controller.persistence.TrialNotifications.State.UNKNOWN;
-
-/**
- * Expires unused tenants from Vespa Cloud.
- *
- * @author ogronnesby
- */
-public class CloudTrialExpirer extends ControllerMaintainer {
- private static final Logger log = Logger.getLogger(CloudTrialExpirer.class.getName());
-
- private static final Duration nonePlanAfter = Duration.ofDays(14);
- private static final Duration tombstoneAfter = Duration.ofDays(91);
- private final ListFlag<String> extendedTrialTenants;
- private final BooleanFlag cloudTrialNotificationEnabled;
-
- public CloudTrialExpirer(Controller controller, Duration interval) {
- super(controller, interval, null, SystemName.allOf(SystemName::isPublic));
- this.extendedTrialTenants = PermanentFlags.EXTENDED_TRIAL_TENANTS.bindTo(controller().flagSource());
- this.cloudTrialNotificationEnabled = Flags.CLOUD_TRIAL_NOTIFICATIONS.bindTo(controller().flagSource());
- }
-
- @Override
- protected double maintain() {
- var a = tombstoneNonePlanTenants();
- var b = moveInactiveTenantsToNonePlan();
- var c = notifyTenants();
- return (a ? 0.0 : -(1D/3)) + (b ? 0.0 : -(1D/3) + (c ? 0.0 : -(1D/3)));
- }
-
- private boolean moveInactiveTenantsToNonePlan() {
- var idleTrialTenants = controller().tenants().asList().stream()
- .filter(this::tenantIsCloudTenant)
- .filter(this::tenantIsNotExemptFromExpiry)
- .filter(this::tenantHasNoDeployments)
- .filter(this::tenantHasTrialPlan)
- .filter(tenantReadersNotLoggedIn(nonePlanAfter))
- .toList();
-
- if (! idleTrialTenants.isEmpty()) {
- var tenants = idleTrialTenants.stream().map(Tenant::name).map(TenantName::value).collect(Collectors.joining(", "));
- log.info("Setting tenants to 'none' plan: " + tenants);
- }
-
- return setPlanNone(idleTrialTenants);
- }
-
- private boolean tombstoneNonePlanTenants() {
- var idleOldPlanTenants = controller().tenants().asList().stream()
- .filter(this::tenantIsCloudTenant)
- .filter(this::tenantIsNotExemptFromExpiry)
- .filter(this::tenantHasNoDeployments)
- .filter(this::tenantHasNonePlan)
- .filter(tenantReadersNotLoggedIn(tombstoneAfter))
- .toList();
-
- if (! idleOldPlanTenants.isEmpty()) {
- var tenants = idleOldPlanTenants.stream().map(Tenant::name).map(TenantName::value).collect(Collectors.joining(", "));
- log.info("Setting tenants as tombstoned: " + tenants);
- }
-
- return tombstoneTenants(idleOldPlanTenants);
- }
-
- /*
- * Trial plan notification states. Transition to a new state triggers a notification/email
- * - SIGNED_UP: Tenant has signed up for trial
- * - MID_CHECK_IN: Tenant is halfway through trial (7 days)
- * - EXPIRES_IMMEDIATELY: Tenant has 1 day left of trial
- * - EXPIRED: Tenant has expired
- */
- private boolean notifyTenants() {
- try {
- var currentStatus = controller().curator().readTrialNotifications()
- .map(TrialNotifications::tenants).orElse(List.of());
- log.fine(() -> "Current: %s".formatted(currentStatus));
- var currentStatusByTenant = new HashMap<TenantName, TrialNotifications.Status>();
- currentStatus.forEach(status -> currentStatusByTenant.put(status.tenant(), status));
- var updatedStatus = new ArrayList<TrialNotifications.Status>();
- var now = controller().clock().instant();
-
- for (var tenant : controller().tenants().asList()) {
-
- var status = currentStatusByTenant.get(tenant.name());
- var state = status == null ? UNKNOWN : status.state();
- var plan = controller().serviceRegistry().billingController().getPlan(tenant.name()).value();
- var ageInDays = Duration.between(tenant.createdAt(), now).toDays();
-
- // TODO Replace stubs with proper email content stored in templates.
-
- var enabled = cloudTrialNotificationEnabled.with(FetchVector.Dimension.TENANT_ID, tenant.name().value()).value();
- if (!enabled) {
- if (status != null) updatedStatus.add(status);
- } else if (!List.of("none", "trial").contains(plan)) {
- // Ignore tenants that are on a paid plan and skip from inclusion in updated data structure
- } else if (status == null && "trial".equals(plan) && ageInDays <= 1) {
- updatedStatus.add(updatedStatus(tenant, now, SIGNED_UP));
- notifySignup(tenant);
- } else if ("none".equals(plan) && !List.of(EXPIRED).contains(state)) {
- updatedStatus.add(updatedStatus(tenant, now, EXPIRED));
- notifyExpired(tenant);
- } else if ("trial".equals(plan) && ageInDays >= 13
- && !List.of(EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) {
- updatedStatus.add(updatedStatus(tenant, now, EXPIRES_IMMEDIATELY));
- notifyExpiresImmediately(tenant);
- } else if ("trial".equals(plan) && ageInDays >= 7
- && !List.of(MID_CHECK_IN, EXPIRES_IMMEDIATELY, EXPIRED).contains(state)) {
- updatedStatus.add(updatedStatus(tenant, now, MID_CHECK_IN));
- notifyMidCheckIn(tenant);
- } else {
- updatedStatus.add(status);
- }
- }
- log.fine(() -> "Updated: %s".formatted(updatedStatus));
- controller().curator().writeTrialNotifications(new TrialNotifications(updatedStatus));
- return true;
- } catch (Exception e) {
- log.log(Level.WARNING, "Failed to process trial notifications", e);
- return false;
- }
- }
-
- private void notifySignup(Tenant tenant) {
- var consoleMsg = "Welcome to Vespa Cloud trial! [Manage plan](%s)".formatted(billingUrl(tenant));
- queueNotification(tenant, consoleMsg, "Welcome to Vespa Cloud", MailTemplating.Template.TRIAL_SIGNED_UP);
- }
-
- private void notifyMidCheckIn(Tenant tenant) {
- var consoleMsg = "You're halfway through the **14 day** trial period. [Manage plan](%s)".formatted(billingUrl(tenant));
- queueNotification(tenant, consoleMsg, "How is your Vespa Cloud trial going?", MailTemplating.Template.TRIAL_MIDWAY_CHECKIN);
- }
-
- private void notifyExpiresImmediately(Tenant tenant) {
- var consoleMsg = "Your Vespa Cloud trial expires **tomorrow**. [Manage plan](%s)".formatted(billingUrl(tenant));
- queueNotification(tenant, consoleMsg, "Your Vespa Cloud trial expires tomorrow", MailTemplating.Template.TRIAL_EXPIRES_IMMEDIATELY);
- }
-
- private void notifyExpired(Tenant tenant) {
- var consoleMsg = "Your Vespa Cloud trial has expired. [Upgrade plan](%s)".formatted(billingUrl(tenant));
- queueNotification(tenant, consoleMsg, "Your Vespa Cloud trial has expired", MailTemplating.Template.TRIAL_EXPIRED);
- }
-
- private void queueNotification(Tenant tenant, String consoleMsg, String emailSubject, MailTemplating.Template template) {
- var mail = Optional.of(Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT)
- .subject(emailSubject)
- .with("mailMessageTemplate", template.getId())
- .with("mailTitle", emailSubject)
- .with("consoleLink", controller().serviceRegistry().consoleUrls().tenantOverview(tenant.name()))
- .build());
- var source = NotificationSource.from(tenant.name());
- // Remove previous notification to ensure new notification is sent by email
- controller().notificationsDb().removeNotification(source, Notification.Type.account);
- controller().notificationsDb().setNotification(
- source, Notification.Type.account, Notification.Level.info, consoleMsg, List.of(), mail);
- }
-
- private String billingUrl(Tenant t) { return controller().serviceRegistry().consoleUrls().tenantBilling(t.name()); }
-
- private static TrialNotifications.Status updatedStatus(Tenant t, Instant i, TrialNotifications.State s) {
- return new TrialNotifications.Status(t.name(), s, i);
- }
-
- private boolean tenantIsCloudTenant(Tenant tenant) {
- return tenant.type() == Tenant.Type.cloud;
- }
-
- private Predicate<Tenant> tenantReadersNotLoggedIn(Duration duration) {
- // returns true if no user has logged in to the tenant after (now - duration)
- return (Tenant tenant) -> {
- var timeLimit = controller().clock().instant().minus(duration);
- return tenant.lastLoginInfo().get(LastLoginInfo.UserLevel.user)
- .map(instant -> instant.isBefore(timeLimit))
- .orElse(false);
- };
- }
-
- private boolean tenantHasTrialPlan(Tenant tenant) {
- var planId = controller().serviceRegistry().billingController().getPlan(tenant.name());
- return "trial".equals(planId.value());
- }
-
- private boolean tenantHasNonePlan(Tenant tenant) {
- var planId = controller().serviceRegistry().billingController().getPlan(tenant.name());
- return "none".equals(planId.value());
- }
-
- private boolean tenantIsNotExemptFromExpiry(Tenant tenant) {
- return !extendedTrialTenants.value().contains(tenant.name().value());
- }
-
- private boolean tenantHasNoDeployments(Tenant tenant) {
- return controller().applications().asList(tenant.name()).stream()
- .flatMap(app -> app.instances().values().stream())
- .mapToLong(instance -> instance.deployments().values().size())
- .sum() == 0;
- }
-
- private boolean setPlanNone(List<Tenant> tenants) {
- var success = true;
- for (var tenant : tenants) {
- try {
- controller().serviceRegistry().billingController().setPlan(tenant.name(), PlanId.from("none"), false, false);
- } catch (RuntimeException e) {
- log.info("Could not change plan for " + tenant.name() + ": " + e.getMessage());
- success = false;
- }
- }
- return success;
- }
-
- private boolean tombstoneTenants(List<Tenant> tenants) {
- var success = true;
- for (var tenant : tenants) {
- success &= deleteApplicationsWithNoDeployments(tenant);
- log.fine("Tombstoning empty tenant: " + tenant.name());
- try {
- controller().tenants().delete(tenant.name(), Optional.empty(), false);
- } catch (RuntimeException e) {
- log.info("Could not tombstone tenant " + tenant.name() + ": " + e.getMessage());
- success = false;
- }
- }
- return success;
- }
-
- private boolean deleteApplicationsWithNoDeployments(Tenant tenant) {
- // this method only removes applications with no active deployments in them
- var success = true;
- for (var application : controller().applications().asList(tenant.name())) {
- try {
- log.fine("Removing empty application: " + application.id());
- controller().applications().deleteApplication(application.id(), Optional.empty());
- } catch (RuntimeException e) {
- log.info("Could not removing application " + application.id() + ": " + e.getMessage());
- success = false;
- }
- }
- return success;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java
deleted file mode 100644
index e0db7780fbb..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainer.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.TenantController;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.ContactRetriever;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.Optional;
-import java.util.function.Predicate;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import static java.util.logging.Level.FINE;
-
-/**
- * Periodically fetch and store contact information for tenants.
- *
- * @author mpolden
- */
-public class ContactInformationMaintainer extends ControllerMaintainer {
-
- private static final Logger log = Logger.getLogger(ContactInformationMaintainer.class.getName());
-
- private final ContactRetriever contactRetriever;
-
- public ContactInformationMaintainer(Controller controller, Duration interval, Double successFactorBaseline) {
- super(controller, interval, null, SystemName.allOf(Predicate.not(SystemName::isPublic)), successFactorBaseline);
- this.contactRetriever = controller.serviceRegistry().contactRetriever();
- }
-
- @Override
- protected double maintain() {
- TenantController tenants = controller().tenants();
- int attempts = 0;
- int failures = 0;
- for (Tenant tenant : tenants.asList()) {
- log.log(FINE, () -> "Updating contact information for " + tenant);
- try {
- attempts++;
- switch (tenant.type()) {
- case athenz:
- tenants.lockIfPresent(tenant.name(), LockedTenant.Athenz.class, lockedTenant -> {
- Contact contact = contactRetriever.getContact(lockedTenant.get().propertyId());
- log.log(FINE, () -> "Contact found for " + tenant + " was " +
- (Optional.of(contact).equals(tenant.contact()) ? "un" : "") + "changed");
- tenants.store(lockedTenant.with(contact));
- });
- break;
- case cloud:
- break;
- default:
- throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'.");
- }
- } catch (Exception e) {
- failures++;
- log.log(Level.WARNING, "Failed to update contact information for " + tenant + ": " +
- Exceptions.toMessageString(e) + ". Retrying in " +
- interval());
- }
- }
- return asSuccessFactorDeviation(attempts, failures);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainer.java
deleted file mode 100644
index 3bc9126f835..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainer.java
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.concurrent.maintenance.JobMetrics;
-import com.yahoo.concurrent.maintenance.Maintainer;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.hosted.controller.Controller;
-
-import java.time.Duration;
-import java.util.EnumSet;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-/**
- * A maintainer is some job which runs at a fixed interval to perform some maintenance task in the controller.
- *
- * @author bratseth
- */
-public abstract class ControllerMaintainer extends Maintainer {
-
- private final Controller controller;
-
- /** The systems in which this maintainer should run */
- private final Set<SystemName> activeSystems;
-
-
- public ControllerMaintainer(Controller controller, Duration interval) {
- this(controller, interval, null, EnumSet.allOf(SystemName.class), 1.0);
- }
-
- public ControllerMaintainer(Controller controller, Duration interval, Double successFactorBaseline) {
- this(controller, interval, null, EnumSet.allOf(SystemName.class), successFactorBaseline);
- }
-
- public ControllerMaintainer(Controller controller, Duration interval, String name, Set<SystemName> activeSystems) {
- this(controller, interval, name, activeSystems, 1.0);
- }
-
- public ControllerMaintainer(Controller controller, Duration interval, String name, Set<SystemName> activeSystems, double successFactorBaseline) {
- super(name, interval, controller.clock(), controller.jobControl(),
- new ControllerJobMetrics(controller.metric()), controller.curator().cluster(), true, successFactorBaseline);
- this.controller = controller;
- this.activeSystems = Set.copyOf(Objects.requireNonNull(activeSystems));
- }
-
- protected Controller controller() { return controller; }
-
- @Override
- public void run() {
- if (!activeSystems.contains(controller.system())) return;
- super.run();
- }
-
- private static class ControllerJobMetrics extends JobMetrics {
-
- private final Metric metric;
-
- public ControllerJobMetrics(Metric metric) {
- this.metric = metric;
- }
-
- @Override
- public void completed(String job, double successFactorDeviation, long durationMs) {
- metric.set("maintenance.successFactorDeviation", successFactorDeviation, metric.createContext(Map.of("job", job)));
- metric.set("maintenance.duration", durationMs, metric.createContext(Map.of("job", job)));
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
deleted file mode 100644
index 8d45fcb8878..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java
+++ /dev/null
@@ -1,221 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.concurrent.maintenance.Maintainer;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.api.integration.user.UserManagement;
-
-import java.time.Duration;
-import java.time.temporal.TemporalUnit;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.stream.Collectors;
-
-import static java.time.temporal.ChronoUnit.HOURS;
-import static java.time.temporal.ChronoUnit.MINUTES;
-import static java.time.temporal.ChronoUnit.SECONDS;
-
-/**
- * Maintenance jobs of the controller.
- * Each maintenance job is a singleton instance of its implementing class, created and owned by this,
- * and running its own dedicated thread.
- *
- * @author bratseth
- */
-public class ControllerMaintenance extends AbstractComponent {
-
- private final Upgrader upgrader;
- private final OsUpgradeScheduler osUpgradeScheduler;
- private final List<Maintainer> maintainers = new CopyOnWriteArrayList<>();
-
- @Inject
- @SuppressWarnings("unused") // instantiated by Dependency Injection
- public ControllerMaintenance(Controller controller, Metric metric, UserManagement userManagement, AthenzClientFactory athenzClientFactory) {
- Intervals intervals = new Intervals(controller.system());
- SuccessFactorBaseline successFactorBaseline = new SuccessFactorBaseline(controller.system());
- upgrader = new Upgrader(controller, intervals.defaultInterval);
- osUpgradeScheduler = new OsUpgradeScheduler(controller, intervals.osUpgradeScheduler);
- maintainers.add(upgrader);
- maintainers.add(osUpgradeScheduler);
- maintainers.addAll(osUpgraders(controller, intervals.osUpgrader));
- maintainers.add(new DeploymentExpirer(controller, intervals.defaultInterval));
- maintainers.add(new DeploymentInfoMaintainer(controller, intervals.deploymentInfoMaintainer, successFactorBaseline.deploymentInfoMaintainerBaseline));
- maintainers.add(new DeploymentUpgrader(controller, intervals.defaultInterval));
- maintainers.add(new DeploymentIssueReporter(controller, controller.serviceRegistry().deploymentIssues(), intervals.defaultInterval));
- maintainers.add(new MetricsReporter(controller, metric, athenzClientFactory.createZmsClient()));
- maintainers.add(new OutstandingChangeDeployer(controller, intervals.outstandingChangeDeployer));
- maintainers.add(new VersionStatusUpdater(controller, intervals.versionStatusUpdater));
- maintainers.add(new ReadyJobsTrigger(controller, intervals.readyJobsTrigger));
- maintainers.add(new DeploymentMetricsMaintainer(controller, intervals.deploymentMetricsMaintainer, successFactorBaseline.deploymentMetricsMaintainerBaseline));
- maintainers.add(new ApplicationOwnershipConfirmer(controller, intervals.applicationOwnershipConfirmer, controller.serviceRegistry().ownershipIssues()));
- maintainers.add(new SystemUpgrader(controller, intervals.systemUpgrader));
- maintainers.add(new JobRunner(controller, intervals.jobRunner));
- maintainers.add(new OsVersionStatusUpdater(controller, intervals.osVersionStatusUpdater));
- maintainers.add(new ContactInformationMaintainer(controller, intervals.contactInformationMaintainer, successFactorBaseline.contactInformationMaintainerBaseline));
- maintainers.add(new NameServiceDispatcher(controller, intervals.nameServiceDispatcher));
- maintainers.add(new CostReportMaintainer(controller, intervals.costReportMaintainer, controller.serviceRegistry().costReportConsumer()));
- maintainers.add(new ResourceMeterMaintainer(controller, intervals.resourceMeterMaintainer, metric, controller.serviceRegistry().resourceDatabase()));
- maintainers.add(new ResourceTagMaintainer(controller, intervals.resourceTagMaintainer, controller.serviceRegistry().resourceTagger()));
- maintainers.add(new ApplicationMetaDataGarbageCollector(controller, intervals.applicationMetaDataGarbageCollector));
- maintainers.add(new ArtifactExpirer(controller, intervals.containerImageExpirer));
- maintainers.add(new HostInfoUpdater(controller, intervals.hostInfoUpdater));
- maintainers.add(new ReindexingTriggerer(controller, intervals.reindexingTriggerer));
- maintainers.add(new EndpointCertificateMaintainer(controller, intervals.endpointCertificateMaintainer));
- maintainers.add(new BcpGroupUpdater(controller, intervals.trafficFractionUpdater, successFactorBaseline.trafficFractionUpdater));
- maintainers.add(new ArchiveUriUpdater(controller, intervals.archiveUriUpdater));
- maintainers.add(new ArchiveAccessMaintainer(controller, metric, intervals.archiveAccessMaintainer));
- maintainers.add(new TenantRoleMaintainer(controller, intervals.tenantRoleMaintainer));
- maintainers.add(new TenantRoleCleanupMaintainer(controller, intervals.tenantRoleMaintainer));
- maintainers.add(new ChangeRequestMaintainer(controller, intervals.changeRequestMaintainer));
- maintainers.add(new VcmrMaintainer(controller, intervals.vcmrMaintainer, metric));
- maintainers.add(new CloudDatabaseMaintainer(controller, intervals.defaultInterval));
- maintainers.add(new CloudTrialExpirer(controller, intervals.defaultInterval));
- maintainers.add(new RetriggerMaintainer(controller, intervals.retriggerMaintainer));
- maintainers.add(new UserManagementMaintainer(controller, intervals.userManagementMaintainer, controller.serviceRegistry().roleMaintainer()));
- maintainers.add(new BillingDatabaseMaintainer(controller, intervals.billingDatabaseMaintainer));
- maintainers.add(new MeteringMonitorMaintainer(controller, intervals.meteringMonitorMaintainer, controller.serviceRegistry().resourceDatabase(), metric));
- maintainers.add(new EnclaveAccessMaintainer(controller, intervals.defaultInterval));
- maintainers.add(new CertificatePoolMaintainer(controller, metric, intervals.certificatePoolMaintainer));
- maintainers.add(new BillingReportMaintainer(controller, intervals.billingReportMaintainer));
- maintainers.add(new CloudAccountVerifier(controller, intervals.cloudAccountVerifier));
- maintainers.add(new DataPlaneTokenRedeployer(controller, intervals.dataPlaneTokenRedeployer));
- }
-
- public Upgrader upgrader() { return upgrader; }
-
- public OsUpgradeScheduler osUpgradeScheduler() { return osUpgradeScheduler; }
-
- @Override
- public void deconstruct() {
- maintainers.forEach(Maintainer::shutdown);
- maintainers.forEach(Maintainer::awaitShutdown);
- }
-
- /** Create one OS upgrader per cloud found in the zone registry of controller */
- private static List<OsUpgrader> osUpgraders(Controller controller, Duration interval) {
- return controller.zoneRegistry().zones().controllerUpgraded().zones().stream()
- .map(ZoneApi::getCloudName)
- .distinct()
- .sorted()
- .map(cloud -> new OsUpgrader(controller, interval, cloud))
- .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
- }
-
- private static class Intervals {
-
- private static final Duration MAX_CD_INTERVAL = Duration.ofHours(1);
-
- private final SystemName system;
-
- private final Duration defaultInterval;
- private final Duration deploymentInfoMaintainer;
- private final Duration outstandingChangeDeployer;
- private final Duration versionStatusUpdater;
- private final Duration readyJobsTrigger;
- private final Duration deploymentMetricsMaintainer;
- private final Duration applicationOwnershipConfirmer;
- private final Duration systemUpgrader;
- private final Duration jobRunner;
- private final Duration osVersionStatusUpdater;
- private final Duration osUpgrader;
- private final Duration osUpgradeScheduler;
- private final Duration contactInformationMaintainer;
- private final Duration nameServiceDispatcher;
- private final Duration costReportMaintainer;
- private final Duration resourceMeterMaintainer;
- private final Duration resourceTagMaintainer;
- private final Duration applicationMetaDataGarbageCollector;
- private final Duration containerImageExpirer;
- private final Duration hostInfoUpdater;
- private final Duration reindexingTriggerer;
- private final Duration endpointCertificateMaintainer;
- private final Duration trafficFractionUpdater;
- private final Duration archiveUriUpdater;
- private final Duration archiveAccessMaintainer;
- private final Duration tenantRoleMaintainer;
- private final Duration changeRequestMaintainer;
- private final Duration vcmrMaintainer;
- private final Duration retriggerMaintainer;
- private final Duration userManagementMaintainer;
- private final Duration billingDatabaseMaintainer;
- private final Duration meteringMonitorMaintainer;
- private final Duration certificatePoolMaintainer;
- private final Duration billingReportMaintainer;
- private final Duration cloudAccountVerifier;
- private final Duration dataPlaneTokenRedeployer;
-
- public Intervals(SystemName system) {
- this.system = Objects.requireNonNull(system);
- this.defaultInterval = duration(system.isCd() ? 1 : 5, MINUTES);
- this.deploymentInfoMaintainer = duration(system.isCd() ? 1 : 10, MINUTES);
- this.outstandingChangeDeployer = duration(3, MINUTES);
- this.versionStatusUpdater = duration(3, MINUTES);
- this.readyJobsTrigger = duration(1, MINUTES);
- this.deploymentMetricsMaintainer = duration(10, MINUTES);
- this.applicationOwnershipConfirmer = duration(3, HOURS);
- this.systemUpgrader = duration(2, MINUTES);
- this.jobRunner = duration(system.isCd() ? 45 : 90, SECONDS);
- this.osVersionStatusUpdater = duration(2, MINUTES);
- this.osUpgrader = duration(1, MINUTES);
- this.osUpgradeScheduler = duration(15, MINUTES);
- this.contactInformationMaintainer = duration(12, HOURS);
- this.nameServiceDispatcher = duration(10, SECONDS);
- this.costReportMaintainer = duration(2, HOURS);
- this.resourceMeterMaintainer = duration(3, MINUTES);
- this.resourceTagMaintainer = duration(30, MINUTES);
- this.applicationMetaDataGarbageCollector = duration(12, HOURS);
- this.containerImageExpirer = duration(12, HOURS);
- this.hostInfoUpdater = duration(12, HOURS);
- this.reindexingTriggerer = duration(1, HOURS);
- this.endpointCertificateMaintainer = duration(1, HOURS);
- this.trafficFractionUpdater = duration(5, MINUTES);
- this.archiveUriUpdater = duration(5, MINUTES);
- this.archiveAccessMaintainer = duration(10, MINUTES);
- this.tenantRoleMaintainer = duration(5, MINUTES);
- this.changeRequestMaintainer = duration(1, HOURS);
- this.vcmrMaintainer = duration(1, HOURS);
- this.retriggerMaintainer = duration(1, MINUTES);
- this.userManagementMaintainer = duration(12, HOURS);
- this.billingDatabaseMaintainer = duration(5, MINUTES);
- this.meteringMonitorMaintainer = duration(30, MINUTES);
- this.certificatePoolMaintainer = duration(15, MINUTES);
- this.billingReportMaintainer = duration(60, MINUTES);
- this.cloudAccountVerifier = duration(10, MINUTES);
- this.dataPlaneTokenRedeployer = duration(1, MINUTES);
- }
-
- private Duration duration(long amount, TemporalUnit unit) {
- Duration duration = Duration.of(amount, unit);
- if (system.isCd() && duration.compareTo(MAX_CD_INTERVAL) > 0) {
- return MAX_CD_INTERVAL; // Ensure that maintainer is given enough time to run in CD
- }
- return duration;
- }
-
- }
-
- private static class SuccessFactorBaseline {
-
- private final Double deploymentMetricsMaintainerBaseline;
- private final Double trafficFractionUpdater;
- private final Double deploymentInfoMaintainerBaseline;
- private final Double contactInformationMaintainerBaseline;
-
- public SuccessFactorBaseline(SystemName system) {
- Objects.requireNonNull(system);
- this.deploymentMetricsMaintainerBaseline = 0.90;
- this.trafficFractionUpdater = system.isCd() ? 0.5 : 0.65;
- this.deploymentInfoMaintainerBaseline = system.isCd() ? 0.5 : 0.95;
- this.contactInformationMaintainerBaseline = 0.95;
- }
-
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainer.java
deleted file mode 100644
index af8248c399c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainer.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumer;
-import com.yahoo.vespa.hosted.controller.metric.CostCalculator;
-
-import java.time.Clock;
-import java.time.Duration;
-import java.util.EnumSet;
-
-/**
- * Periodically calculate and store cost allocation for properties.
- *
- * @author ldalves
- * @author andreer
- */
-public class CostReportMaintainer extends ControllerMaintainer {
-
- private final CostReportConsumer consumer;
- private final NodeRepository nodeRepository;
- private final Clock clock;
-
- public CostReportMaintainer(Controller controller, Duration interval, CostReportConsumer costReportConsumer) {
- super(controller, interval, null, EnumSet.of(SystemName.main));
- this.consumer = costReportConsumer;
- this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- this.clock = controller.clock();
- }
-
- @Override
- protected double maintain() {
- var csv = CostCalculator.resourceShareByPropertyToCsv(nodeRepository, controller(), clock, consumer.fixedAllocations());
- consumer.consume(csv);
- return 0.0;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DataPlaneTokenRedeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DataPlaneTokenRedeployer.java
deleted file mode 100644
index e9d2dc0714b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DataPlaneTokenRedeployer.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-
-import java.time.Duration;
-
-/**
- * @author jonmv
- */
-public class DataPlaneTokenRedeployer extends ControllerMaintainer {
-
- public DataPlaneTokenRedeployer(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- protected double maintain() {
- controller().dataplaneTokenService().triggerTokenChangeDeployments();
- return 0;
- }
-
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java
deleted file mode 100644
index aea23e6def8..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirer.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.Optional;
-import java.util.logging.Level;
-
-/**
- * Expires instances in zones that have configured expiration using TimeToLive.
- *
- * @author mortent
- * @author bratseth
- */
-public class DeploymentExpirer extends ControllerMaintainer {
-
- public DeploymentExpirer(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- protected double maintain() {
- int attempts = 0;
- int failures = 0;
- for (Application application : controller().applications().readable()) {
- for (Instance instance : application.instances().values())
- for (Deployment deployment : instance.deployments().values()) {
- if (!isExpired(deployment, instance.id())) continue;
-
- try {
- log.log(Level.INFO, "Expiring deployment of " + instance.id() + " in " + deployment.zone());
- attempts++;
- controller().applications().deactivate(instance.id(), deployment.zone());
- } catch (Exception e) {
- failures++;
- log.log(Level.WARNING, "Could not expire " + deployment + " of " + instance +
- ": " + Exceptions.toMessageString(e) + ". Retrying in " +
- interval());
- }
- }
- }
- return asSuccessFactorDeviation(attempts, failures);
- }
-
- /** Returns whether given deployment has expired according to its TTL */
- private boolean isExpired(Deployment deployment, ApplicationId instance) {
- if (deployment.zone().environment().isProduction()) return false; // Never expire production deployments
-
- Optional<Duration> ttl = controller().zoneRegistry().getDeploymentTimeToLive(deployment.zone());
- if (ttl.isEmpty()) return false;
-
- return controller().jobController().lastDeploymentStart(instance, deployment)
- .plus(ttl.get()).isBefore(controller().clock().instant());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentInfoMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentInfoMaintainer.java
deleted file mode 100644
index 7b4ed9e1e98..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentInfoMaintainer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.Collection;
-import java.util.Map;
-
-/**
- * This pulls application deployment information from the node repo on all config servers,
- * and stores it in memory in controller.applications().deploymentInfo().
- *
- * @author bratseth
- */
-public class DeploymentInfoMaintainer extends ControllerMaintainer {
-
- private final NodeRepository nodeRepository;
-
- public DeploymentInfoMaintainer(Controller controller, Duration duration, Double successFactorBaseline) {
- super(controller, duration, successFactorBaseline);
- this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- }
-
- @Override
- protected double maintain() {
- int attempts = 0;
- int failures = 0;
- outer:
- for (var application : controller().applications().idList()) {
- for (var instance : controller().applications().getApplication(application).map(Application::instances).orElse(Map.of()).values()) {
- for (var deployment : instanceDeployments(instance)) {
- if (shuttingDown()) break outer;
- attempts++;
- if ( ! updateDeploymentInfo(deployment))
- failures++;
- }
- }
- }
- return asSuccessFactorDeviation(attempts, failures);
- }
-
- private Collection<DeploymentId> instanceDeployments(Instance instance) {
- return instance.deployments().keySet().stream()
- .filter(zoneId -> ! zoneId.environment().isTest())
- .map(zoneId -> new DeploymentId(instance.id(), zoneId))
- .toList();
- }
-
- private boolean updateDeploymentInfo(DeploymentId id) {
- try {
- controller().applications().deploymentInfo().put(id, nodeRepository.getApplication(id.zoneId(), id.applicationId()));
- return true;
- }
- catch (ConfigServerException e) {
- log.info("Could not retrieve deployment info for " + id + ": " + Exceptions.toMessageString(e));
- return false;
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java
deleted file mode 100644
index ae9eb1dc2b5..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporter.java
+++ /dev/null
@@ -1,157 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.DeploymentIssues;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatusList;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.Collection;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.logging.Level;
-
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.broken;
-
-/**
- * Maintenance job which files issues for tenants when they have jobs which fails continuously
- * and escalates issues which are not handled in a timely manner.
- *
- * @author jonmv
- */
-public class DeploymentIssueReporter extends ControllerMaintainer {
-
- static final Duration maxFailureAge = Duration.ofDays(2);
- static final Duration maxInactivity = Duration.ofDays(4);
- static final Duration upgradeGracePeriod = Duration.ofHours(2);
-
- private final DeploymentIssues deploymentIssues;
-
- DeploymentIssueReporter(Controller controller, DeploymentIssues deploymentIssues, Duration maintenanceInterval) {
- super(controller, maintenanceInterval);
- this.deploymentIssues = deploymentIssues;
- }
-
- @Override
- protected double maintain() {
- return ( maintainDeploymentIssues(applications()) +
- maintainPlatformIssue(applications()) +
- escalateInactiveDeploymentIssues(applications()))
- / 3;
- }
-
- /** Returns the applications to maintain issue status for. */
- private List<Application> applications() {
- return ApplicationList.from(controller().applications().readable())
- .withProjectId()
- .matching(appliaction -> appliaction.deploymentSpec().steps().stream().anyMatch(step -> step.concerns(Environment.prod)))
- .asList();
- }
-
- /**
- * File issues for applications which have failed deployment for longer than maxFailureAge
- * and store the issue id for the filed issues. Also, clear the issueIds of applications
- * where deployment has not failed for this amount of time.
- */
- private double maintainDeploymentIssues(List<Application> applications) {
- List<TenantAndApplicationId> failingApplications = controller().jobController().deploymentStatuses(ApplicationList.from(applications))
- .matching(status -> ! status.jobSteps().isEmpty())
- .failingApplicationChangeSince(controller().clock().instant().minus(maxFailureAge))
- .mapToList(status -> status.application().id());
-
- for (Application application : applications)
- if (failingApplications.contains(application.id()))
- fileDeploymentIssueFor(application);
- else
- store(application.id(), null);
- return 0.0;
- }
-
- /**
- * When the confidence for the system version is BROKEN, file an issue listing the
- * applications that have been failing the upgrade to the system version for
- * longer than the set grace period, or update this list if the issue already exists.
- */
- private double maintainPlatformIssue(List<Application> applications) {
- if (controller().system() == SystemName.cd)
- return 0.0;
-
- VersionStatus versionStatus = controller().readVersionStatus();
- Version systemVersion = controller().systemVersion(versionStatus);
-
- if (versionStatus.version(systemVersion).confidence() != broken)
- return 0.0;
-
- DeploymentStatusList statuses = controller().jobController().deploymentStatuses(ApplicationList.from(applications));
- if (statuses.failingUpgradeToVersionSince(systemVersion, controller().clock().instant().minus(upgradeGracePeriod)).isEmpty())
- return 0.0;
-
- List<ApplicationId> failingApplications = statuses.failingUpgradeToVersionSince(systemVersion, controller().clock().instant())
- .mapToList(status -> status.application().id().defaultInstance());
-
- // TODO jonmv: Send only tenant and application, here and elsewhere in this.
- deploymentIssues.fileUnlessOpen(failingApplications, systemVersion);
- return 0.0;
- }
-
- private Tenant ownerOf(TenantAndApplicationId applicationId) {
- return controller().tenants().get(applicationId.tenant())
- .orElseThrow(() -> new IllegalStateException("No tenant found for application " + applicationId));
- }
-
- /** File an issue for applicationId, if it doesn't already have an open issue associated with it. */
- private void fileDeploymentIssueFor(Application application) {
- try {
- Tenant tenant = ownerOf(application.id());
- tenant.contact().ifPresent(contact -> {
- Optional<IssueId> ourIssueId = application.deploymentIssueId();
- IssueId issueId = deploymentIssues.fileUnlessOpen(ourIssueId, application.id().defaultInstance(),
- application.issueOwner().orElse(null), application.userOwner().orElse(null),
- contact);
- store(application.id(), issueId);
- });
- }
- catch (RuntimeException e) { // Catch errors due to wrong data in the controller, or issues client timeout.
- log.log(Level.INFO, "Exception caught when attempting to file an issue for '" + application.id() + "': " + Exceptions.toMessageString(e));
- }
- }
-
- /** Escalate issues for which there has been no activity for a certain amount of time. */
- private double escalateInactiveDeploymentIssues(Collection<Application> applications) {
- AtomicInteger attempts = new AtomicInteger(0);
- AtomicInteger failures = new AtomicInteger(0);
- applications.forEach(application -> application.deploymentIssueId().ifPresent(issueId -> {
- try {
- attempts.incrementAndGet();
- Tenant tenant = ownerOf(application.id());
- deploymentIssues.escalateIfInactive(issueId,
- maxInactivity,
- tenant.type() == Tenant.Type.athenz ? tenant.contact() : Optional.empty());
- }
- catch (RuntimeException e) {
- failures.incrementAndGet();
- log.log(Level.INFO, "Exception caught when attempting to escalate issue with id '" + issueId + "': " + Exceptions.toMessageString(e));
- }
- }));
- return asSuccessFactorDeviation(attempts.get(), failures.get());
- }
-
- private void store(TenantAndApplicationId id, IssueId issueId) {
- controller().applications().lockApplicationIfPresent(id, application ->
- controller().applications().store(application.withDeploymentIssueId(issueId)));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java
deleted file mode 100644
index df1f793914e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainer.java
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.ForkJoinPool;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Function;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * Retrieves deployment metrics such as QPS and document count over the config server API
- * and updates application objects in the controller with this info.
- *
- * @author smorgrav
- * @author mpolden
- */
-public class DeploymentMetricsMaintainer extends ControllerMaintainer {
-
- private static final Logger log = Logger.getLogger(DeploymentMetricsMaintainer.class.getName());
-
- private static final int applicationsToUpdateInParallel = 10;
-
- private final ApplicationController applications;
-
- public DeploymentMetricsMaintainer(Controller controller, Duration duration, Double successFactorBaseline) {
- super(controller, duration, successFactorBaseline);
- this.applications = controller.applications();
- }
-
- public DeploymentMetricsMaintainer(Controller controller, Duration duration) {
- this(controller, duration, 1.0);
- }
-
- @Override
- protected double maintain() {
- AtomicInteger failures = new AtomicInteger(0);
- AtomicInteger attempts = new AtomicInteger(0);
- AtomicReference<Exception> lastException = new AtomicReference<>(null);
-
- // Run parallel stream inside a custom ForkJoinPool so that we can control the number of threads used
- ForkJoinPool pool = new ForkJoinPool(applicationsToUpdateInParallel);
- pool.submit(() ->
- applications.readable().parallelStream().forEach(application -> {
- for (Instance instance : application.instances().values())
- for (Deployment deployment : instance.deployments().values()) {
- attempts.incrementAndGet();
- try {
- DeploymentId deploymentId = new DeploymentId(instance.id(), deployment.zone());
- List<ClusterMetrics> clusterMetrics = controller().serviceRegistry().configServer().getDeploymentMetrics(deploymentId);
- Instant now = controller().clock().instant();
- applications.lockApplicationIfPresent(application.id(), locked -> {
- Deployment existingDeployment = locked.get().require(instance.name()).deployments().get(deployment.zone());
- if (existingDeployment == null) return; // Deployment removed since we started collecting metrics
- DeploymentMetrics newMetrics = updateDeploymentMetrics(existingDeployment.metrics(), clusterMetrics).at(now);
- applications.store(locked.with(instance.name(),
- lockedInstance -> lockedInstance.with(existingDeployment.zone(), newMetrics)
- .recordActivityAt(now, existingDeployment.zone())));
-
- ApplicationReindexing applicationReindexing = controller().serviceRegistry().configServer().getReindexing(deploymentId);
- controller().notificationsDb().setDeploymentMetricsNotifications(deploymentId, clusterMetrics, applicationReindexing);
- });
- } catch (Exception e) {
- failures.incrementAndGet();
- lastException.set(e);
- }
- }
- })
- );
- pool.shutdown();
- try {
- Duration timeout = Duration.ofMinutes(30);
- if (!pool.awaitTermination(timeout.toMillis(), TimeUnit.MILLISECONDS)) {
- log.log(Level.WARNING, "Could not shut down metrics collection thread pool within " + timeout);
- }
- if (lastException.get() != null) {
- log.log(Level.WARNING,
- Text.format("Could not gather metrics for %d/%d deployments. Retrying in %s. Last error: %s",
- failures.get(),
- attempts.get(),
- interval(),
- Exceptions.toMessageString(lastException.get())));
- }
- } catch (InterruptedException e) {
- throw new RuntimeException(e);
- }
- return asSuccessFactorDeviation(attempts.get(), failures.get());
- }
-
- static DeploymentMetrics updateDeploymentMetrics(DeploymentMetrics current, List<ClusterMetrics> metrics) {
- return current
- .withQueriesPerSecond(metrics.stream().flatMap(m -> m.queriesPerSecond().stream()).mapToDouble(Double::doubleValue).sum())
- .withWritesPerSecond(metrics.stream().flatMap(m -> m.feedPerSecond().stream()).mapToDouble(Double::doubleValue).sum())
- .withDocumentCount(metrics.stream().flatMap(m -> m.documentCount().stream()).mapToLong(Double::longValue).sum())
- .withQueryLatencyMillis(weightedAverageLatency(metrics, ClusterMetrics::queriesPerSecond, ClusterMetrics::queryLatency))
- .withWriteLatencyMillis(weightedAverageLatency(metrics, ClusterMetrics::feedPerSecond, ClusterMetrics::feedLatency));
- }
-
- private static double weightedAverageLatency(List<ClusterMetrics> metrics,
- Function<ClusterMetrics, Optional<Double>> rateExtractor,
- Function<ClusterMetrics, Optional<Double>> latencyExtractor) {
- double rateSum = metrics.stream().flatMap(m -> rateExtractor.apply(m).stream()).mapToDouble(Double::longValue).sum();
- if (rateSum == 0) return 0.0;
-
- double weightedLatency = metrics.stream()
- .flatMap(m -> latencyExtractor.apply(m).flatMap(l -> rateExtractor.apply(m).map(r -> l * r)).stream())
- .mapToDouble(Double::doubleValue)
- .sum();
-
- return weightedLatency / rateSum;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java
deleted file mode 100644
index 270c388d73c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgrader.java
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.Versions;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Comparator;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.logging.Level;
-
-/**
- * Upgrades instances in manually deployed zones to the system version, at a convenient time.
- *
- * @author jonmv
- */
-public class DeploymentUpgrader extends ControllerMaintainer {
-
- public DeploymentUpgrader(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- protected double maintain() {
- AtomicInteger attempts = new AtomicInteger();
- AtomicInteger failures = new AtomicInteger();
-
- Version targetPlatform = null; // Upgrade to the newest non-broken, deployable version.
- VersionStatus versionStatus = controller().readVersionStatus();
- for (VespaVersion platform : versionStatus.deployableVersions())
- if (platform.confidence().equalOrHigherThan(VespaVersion.Confidence.normal))
- targetPlatform = platform.versionNumber();
-
- if (targetPlatform == null)
- return 0;
-
- for (Application application : controller().applications().readable())
- for (Instance instance : application.instances().values())
- for (Deployment deployment : instance.deployments().values())
- try {
- JobId job = new JobId(instance.id(), JobType.deploymentTo(deployment.zone()));
- if ( ! deployment.zone().environment().isManuallyDeployed()) continue;
-
- Run last = controller().jobController().last(job).get();
- Versions target = new Versions(targetPlatform, last.versions().targetRevision(), Optional.of(last.versions().targetPlatform()), Optional.of(last.versions().targetRevision()));
- if ( ! last.hasEnded()) continue;
- ApplicationVersion devVersion = application.revisions().get(last.versions().targetRevision());
- if (devVersion.compileVersion()
- .map(version -> controller().applications().versionCompatibility(instance.id()).refuse(version, target.targetPlatform()))
- .orElse(false)) continue;
- if ( devVersion.allowedMajor().isPresent()
- && devVersion.allowedMajor().get() < targetPlatform.getMajor()) continue;
-
- if ( ! deployment.version().isBefore(target.targetPlatform())) continue;
- if ( ! isLikelyNightFor(job)) continue;
- if (deployment.zone().environment() == Environment.perf && ! isIdleOrOutdated(deployment, job)) continue;
-
- log.log(Level.FINE, "Upgrading deployment of " + instance.id() + " in " + deployment.zone());
- attempts.incrementAndGet();
- controller().jobController().start(instance.id(), JobType.deploymentTo(deployment.zone()), target, true, Run.Reason.because("automated upgrade"));
- } catch (Exception e) {
- failures.incrementAndGet();
- log.log(Level.WARNING, "Failed upgrading " + deployment + " of " + instance +
- ": " + Exceptions.toMessageString(e) + ". Retrying in " +
- interval());
- }
- return asSuccessFactorDeviation(attempts.get(), failures.get());
- }
-
- /** Returns whether query and feed metrics are ~zero, or currently platform has been deployed for a week. */
- private boolean isIdleOrOutdated(Deployment deployment, JobId job) {
- if (deployment.metrics().queriesPerSecond() <= 1 && deployment.metrics().writesPerSecond() <= 1) return true;
- return controller().jobController().runs(job).descendingMap().values().stream()
- .takeWhile(run -> run.versions().targetPlatform().equals(deployment.version()))
- .anyMatch(run -> run.start().isBefore(controller().clock().instant().minus(Duration.ofDays(7))));
- }
-
- private boolean isLikelyNightFor(JobId job) {
- int hour = hourOf(controller().clock().instant());
- int[] runStarts = controller().jobController().jobStarts(job).stream()
- .mapToInt(DeploymentUpgrader::hourOf)
- .toArray();
- int localNight = mostLikelyWeeHour(runStarts);
- return Math.abs(hour - localNight) <= 1;
- }
-
- static int mostLikelyWeeHour(int[] starts) {
- double weight = 1;
- double[] buckets = new double[24];
- for (int start : starts)
- buckets[start] += weight *= 0.8; // Weight more recent deployments higher.
-
- int best = -1;
- double min = Double.MAX_VALUE;
- for (int i = 12; i < 36; i++) {
- double sum = 0;
- for (int j = -12; j < 12; j++)
- sum += buckets[(i + j) % 24] / (Math.abs(j) + 1);
-
- if (sum < min) {
- min = sum;
- best = i;
- }
- }
- return (best + 2) % 24;
- }
-
- private static int hourOf(Instant instant) {
- return (int) (instant.toEpochMilli() / 3_600_000 % 24);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainer.java
deleted file mode 100644
index 8fd9dc919fb..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainer.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Duration;
-import java.util.HashSet;
-import java.util.Set;
-import java.util.logging.Logger;
-
-import static java.util.logging.Level.WARNING;
-
-public class EnclaveAccessMaintainer extends ControllerMaintainer {
-
- private static final Logger logger = Logger.getLogger(EnclaveAccessMaintainer.class.getName());
-
- EnclaveAccessMaintainer(Controller controller, Duration interval) {
- super(controller, interval, null, Set.of(SystemName.PublicCd, SystemName.Public));
- }
-
- @Override
- protected double maintain() {
- try {
- return controller().serviceRegistry().enclaveAccessService().allowAccessFor(externalAccounts());
- } catch (RuntimeException e) {
- logger.log(WARNING, "Failed sharing resources with enclave", e);
- return 1.0;
- }
- }
-
- private Set<CloudAccount> externalAccounts() {
- Set<CloudAccount> accounts = new HashSet<>();
- for (Tenant tenant : controller().tenants().asList())
- tenant.cloudAccounts().forEach(accountInfo -> accounts.add(accountInfo.cloudAccount()));
-
- return accounts;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java
deleted file mode 100644
index e3e3e347c04..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainer.java
+++ /dev/null
@@ -1,258 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.google.common.collect.Sets;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.container.jdisc.secretstore.SecretNotFoundException;
-import com.yahoo.container.jdisc.secretstore.SecretStore;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.transaction.NestedTransaction;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateDetails;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.EndpointSecretManager;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.certificate.AssignedCertificate;
-import com.yahoo.vespa.hosted.controller.certificate.UnassignedCertificate;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * Updates refreshed endpoint certificates and triggers redeployment, and deletes unused certificates.
- * <p>
- * See also class EndpointCertificates, which provisions, reprovisions and validates certificates on deploy
- *
- * @author andreer
- */
-public class EndpointCertificateMaintainer extends ControllerMaintainer {
-
- private static final Logger log = Logger.getLogger(EndpointCertificateMaintainer.class.getName());
-
- private final DeploymentTrigger deploymentTrigger;
- private final Clock clock;
- private final CuratorDb curator;
- private final SecretStore secretStore;
- private final EndpointSecretManager endpointSecretManager;
- private final EndpointCertificateProvider endpointCertificateProvider;
- final Comparator<EligibleJob> oldestFirst = Comparator.comparing(e -> e.deployment.at());
-
- @Inject
- public EndpointCertificateMaintainer(Controller controller, Duration interval) {
- super(controller, interval);
- this.deploymentTrigger = controller.applications().deploymentTrigger();
- this.clock = controller.clock();
- this.secretStore = controller.secretStore();
- this.endpointSecretManager = controller.serviceRegistry().secretManager();
- this.curator = controller().curator();
- this.endpointCertificateProvider = controller.serviceRegistry().endpointCertificateProvider();
- }
-
- @Override
- protected double maintain() {
- try {
- // In order of importance
- deployRefreshedCertificates();
- updateRefreshedCertificates();
- deleteUnusedCertificates();
- deleteOrReportUnmanagedCertificates();
- } catch (Exception e) {
- log.log(Level.SEVERE, "Exception caught while maintaining endpoint certificates", e);
- return 1.0;
- }
- return 0.0;
- }
-
- private void updateRefreshedCertificates() {
- curator.readAssignedCertificates().forEach(assignedCertificate -> {
- // Look for and use refreshed certificate
- var latestAvailableVersion = latestVersionInSecretStore(assignedCertificate.certificate());
- if (latestAvailableVersion.isPresent() && latestAvailableVersion.getAsInt() > assignedCertificate.certificate().version()) {
- var refreshedCertificateMetadata = assignedCertificate.certificate()
- .withVersion(latestAvailableVersion.getAsInt())
- .withLastRefreshed(clock.instant().getEpochSecond());
-
- try (Mutex lock = lock(assignedCertificate.application())) {
- if (unchanged(assignedCertificate, lock)) {
- try (NestedTransaction transaction = new NestedTransaction()) {
- curator.writeAssignedCertificate(assignedCertificate.with(refreshedCertificateMetadata), transaction); // Certificate not validated here, but on deploy.
- transaction.commit();
- }
- }
- }
- }
- });
- }
-
- private boolean unchanged(AssignedCertificate assignedCertificate, @SuppressWarnings("unused") Mutex lock) {
- return Optional.of(assignedCertificate).equals(curator.readAssignedCertificate(assignedCertificate.application(), assignedCertificate.instance()));
- }
-
- record EligibleJob(Deployment deployment, ApplicationId applicationId, JobType job) {}
-
- /**
- * If it's been four days since the cert has been refreshed, re-trigger prod deployment jobs (one at a time).
- */
- private void deployRefreshedCertificates() {
- var now = clock.instant();
- var eligibleJobs = new ArrayList<EligibleJob>();
-
- curator.readAssignedCertificates().forEach(assignedCertificate ->
- assignedCertificate.certificate().lastRefreshed().ifPresent(lastRefreshTime -> {
- Instant refreshTime = Instant.ofEpochSecond(lastRefreshTime);
- if (now.isAfter(refreshTime.plus(4, ChronoUnit.DAYS))) {
- if (assignedCertificate.instance().isPresent()) {
- ApplicationId applicationId = assignedCertificate.application().instance(assignedCertificate.instance().get());
- controller().applications().getInstance(applicationId)
- .ifPresent(instance -> instance.productionDeployments().forEach((zone, deployment) -> {
- if (deployment.at().isBefore(refreshTime)) {
- JobType job = JobType.deploymentTo(zone);
- eligibleJobs.add(new EligibleJob(deployment, applicationId, job));
- }
- }));
- } else {
- // This is an application-wide certificate. Trigger all instances
- controller().applications().getApplication(assignedCertificate.application()).ifPresent(application -> {
- application.instances().forEach((ignored, i) -> {
- i.productionDeployments().forEach((zone, deployment) -> {
- if (deployment.at().isBefore(refreshTime)) {
- JobType job = JobType.deploymentTo(zone);
- eligibleJobs.add(new EligibleJob(deployment, i.id(), job));
- }
- });
- });
- });
- }
- }
- }));
-
- eligibleJobs.stream()
- .min(oldestFirst)
- .ifPresent(e -> {
- deploymentTrigger.reTrigger(e.applicationId, e.job, "re-triggered by EndpointCertificateMaintainer");
- log.info("Re-triggering deployment job " + e.job.jobName() + " for instance " +
- e.applicationId.serializedForm() + " to roll out refreshed endpoint certificate");
- });
- }
-
- private OptionalInt latestVersionInSecretStore(EndpointCertificate originalCertificateMetadata) {
- try {
- var certVersions = new HashSet<>(secretStore.listSecretVersions(originalCertificateMetadata.certName()));
- var keyVersions = new HashSet<>(secretStore.listSecretVersions(originalCertificateMetadata.keyName()));
- return Sets.intersection(certVersions, keyVersions).stream().mapToInt(Integer::intValue).max();
- } catch (SecretNotFoundException s) {
- return OptionalInt.empty(); // Likely because the certificate is very recently provisioned - keep current version
- }
- }
-
- private void deleteUnusedCertificates() {
- var oneMonthAgo = clock.instant().minus(30, ChronoUnit.DAYS);
- curator.readAssignedCertificates().forEach(assignedCertificate -> {
- EndpointCertificate certificate = assignedCertificate.certificate();
- var lastRequested = Instant.ofEpochSecond(certificate.lastRequested());
- if (lastRequested.isBefore(oneMonthAgo) && hasNoDeployments(assignedCertificate.application())) {
- try (Mutex lock = lock(assignedCertificate.application())) {
- if (unchanged(assignedCertificate, lock)) {
- log.log(Level.INFO, "Cert for app " + asString(assignedCertificate.application(), assignedCertificate.instance())
- + " has not been requested in a month and app has no deployments, deleting from provider, ZK and secret store");
- endpointCertificateProvider.deleteCertificate(certificate.rootRequestId());
- curator.removeAssignedCertificate(assignedCertificate.application(), assignedCertificate.instance());
- endpointSecretManager.deleteSecret(certificate.certName());
- endpointSecretManager.deleteSecret(certificate.keyName());
- }
- }
- }
- });
- }
-
- private Mutex lock(TenantAndApplicationId application) {
- return curator.lock(application);
- }
-
- private boolean hasNoDeployments(TenantAndApplicationId application) {
- Optional<Application> app = controller().applications().getApplication(application);
- if (app.isEmpty()) return true;
- for (var instance : app.get().instances().values()) {
- if (!instance.deployments().isEmpty()) return false;
- }
- return true;
- }
-
- private void deleteOrReportUnmanagedCertificates() {
- List<EndpointCertificateRequest> requests = endpointCertificateProvider.listCertificates();
- List<AssignedCertificate> assignedCertificates = curator.readAssignedCertificates();
-
- List<String> leafRequestIds = assignedCertificates.stream().map(AssignedCertificate::certificate).flatMap(m -> m.leafRequestId().stream()).toList();
- List<String> rootRequestIds = assignedCertificates.stream().map(AssignedCertificate::certificate).map(EndpointCertificate::rootRequestId).toList();
- List<UnassignedCertificate> unassignedCertificates = curator.readUnassignedCertificates();
- List<String> certPoolRootIds = unassignedCertificates.stream().map(p -> p.certificate().leafRequestId()).flatMap(Optional::stream).toList();
- List<String> certPoolLeafIds = unassignedCertificates.stream().map(p -> p.certificate().rootRequestId()).toList();
-
- var managedIds = new HashSet<String>();
- managedIds.addAll(leafRequestIds);
- managedIds.addAll(rootRequestIds);
- managedIds.addAll(certPoolRootIds);
- managedIds.addAll(certPoolLeafIds);
-
- for (var request : requests) {
- if (!managedIds.contains(request.requestId())) {
-
- // It could just be a refresh we're not aware of yet. See if it matches the cert/keyname of any known cert
- EndpointCertificateDetails unknownCertDetails = endpointCertificateProvider.certificateDetails(request.requestId());
- boolean matchFound = false;
- for (AssignedCertificate assignedCertificate : assignedCertificates) {
- if (assignedCertificate.certificate().certName().equals(unknownCertDetails.certKeyKeyname())) {
- matchFound = true;
- try (Mutex lock = lock(assignedCertificate.application())) {
- if (unchanged(assignedCertificate, lock)) {
- log.log(Level.INFO, "Cert for app " + asString(assignedCertificate.application(), assignedCertificate.instance())
- + " has a new leafRequestId " + unknownCertDetails.requestId() + ", updating in ZK");
- try (NestedTransaction transaction = new NestedTransaction()) {
- EndpointCertificate updated = assignedCertificate.certificate().withLeafRequestId(Optional.of(unknownCertDetails.requestId()));
- curator.writeAssignedCertificate(assignedCertificate.with(updated), transaction);
- transaction.commit();
- }
- }
- break;
- }
- }
- }
- if (!matchFound) {
- // The certificate is not known - however it could be in the process of being requested by us or another controller.
- // So we only delete if it was requested more than 7 days ago.
- if (Instant.parse(request.createTime()).isBefore(Instant.now().minus(7, ChronoUnit.DAYS))) {
- log.log(Level.INFO, String.format("Deleting unmaintained certificate with request_id %s and SANs %s",
- request.requestId(),
- request.dnsNames().stream().map(EndpointCertificateRequest.DnsNameStatus::dnsName).collect(Collectors.joining(", "))));
- endpointCertificateProvider.deleteCertificate(request.requestId());
- }
- }
- }
- }
- }
-
- private static String asString(TenantAndApplicationId application, Optional<InstanceName> instanceName) {
- return application.toString() + instanceName.map(name -> "." + name.value()).orElse("");
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/HostInfoUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/HostInfoUpdater.java
deleted file mode 100644
index 5d6e60ee0bf..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/HostInfoUpdater.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.entity.EntityService;
-import com.yahoo.vespa.hosted.controller.api.integration.entity.NodeEntity;
-
-import java.time.Duration;
-import java.util.EnumSet;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Function;
-import java.util.logging.Logger;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-/**
- * Ensures that the host information for all hosts is up to date.
- *
- * @author mpolden
- * @author bjormel
- */
-public class HostInfoUpdater extends ControllerMaintainer {
-
- private static final Logger LOG = Logger.getLogger(HostInfoUpdater.class.getName());
- private static final Pattern HOST_PATTERN = Pattern.compile("^(proxy|cfg|controller)host(.+)$");
-
- private final NodeRepository nodeRepository;
-
- public HostInfoUpdater(Controller controller, Duration interval) {
- super(controller, interval, null, EnumSet.of(SystemName.cd, SystemName.main));
- this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- }
-
- @Override
- protected double maintain() {
- Map<String, NodeEntity> nodeEntities = controller().serviceRegistry().entityService().listNodes().stream()
- .collect(Collectors.toMap(NodeEntity::hostname,
- Function.identity()));
- int hostsUpdated = 0;
- try {
- for (var zone : controller().zoneRegistry().zones().controllerUpgraded().all().ids()) {
- for (var node : nodeRepository.list(zone, NodeFilter.all())) {
- if (!node.type().isHost()) continue;
- NodeEntity nodeEntity = nodeEntities.get(registeredHostnameOf(node));
- if (nodeEntity == null) continue;
-
- boolean updatedHost = false;
- Optional<String> modelName = modelNameOf(nodeEntity);
- if (modelName.isPresent() && !modelName.equals(node.modelName())) {
- nodeRepository.updateModel(zone, node.hostname().value(), modelName.get());
- updatedHost = true;
- }
-
- Optional<String> switchHostname = nodeEntity.switchHostname();
- if (switchHostname.isPresent() && !switchHostname.equals(node.switchHostname())) {
- nodeRepository.updateSwitchHostname(zone, node.hostname().value(), switchHostname.get());
- updatedHost = true;
- }
-
- if (updatedHost) {
- hostsUpdated++;
- }
- }
- }
- } finally {
- if (hostsUpdated > 0) {
- LOG.info("Updated information for " + hostsUpdated + " hosts(s)");
- }
- }
- return 0.0;
- }
-
- private static Optional<String> modelNameOf(NodeEntity nodeEntity) {
- if (nodeEntity.manufacturer().isEmpty() || nodeEntity.model().isEmpty()) return Optional.empty();
- return Optional.of(nodeEntity.manufacturer().get() + " " + nodeEntity.model().get());
- }
-
- /** Returns the hostname that given host is registered under in the {@link EntityService} */
- private static String registeredHostnameOf(Node host) {
- String hostname = host.hostname().value();
- if (!host.type().isHost()) return hostname;
- Matcher matcher = HOST_PATTERN.matcher(hostname);
- if (!matcher.matches()) return hostname;
- return matcher.replaceFirst("$1$2");
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/InfrastructureUpgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/InfrastructureUpgrader.java
deleted file mode 100644
index 97bb709d423..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/InfrastructureUpgrader.java
+++ /dev/null
@@ -1,174 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.NodeSlice;
-import com.yahoo.config.provision.zone.UpgradePolicy;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.versions.VersionTarget;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.Comparator;
-import java.util.EnumSet;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * Base class for maintainers that upgrade zone infrastructure.
- *
- * @author mpolden
- */
-public abstract class InfrastructureUpgrader<TARGET extends VersionTarget> extends ControllerMaintainer {
-
- private static final Logger log = Logger.getLogger(InfrastructureUpgrader.class.getName());
-
- protected final UpgradePolicy upgradePolicy;
- private final List<SystemApplication> managedApplications;
-
- public InfrastructureUpgrader(Controller controller, Duration interval, UpgradePolicy upgradePolicy,
- List<SystemApplication> managedApplications, String name) {
- super(controller, interval, name, EnumSet.allOf(SystemName.class));
- this.upgradePolicy = upgradePolicy;
- this.managedApplications = List.copyOf(Objects.requireNonNull(managedApplications));
- }
-
- @Override
- protected double maintain() {
- return target().map(target -> upgradeAll(target, managedApplications))
- .orElse(0.0);
- }
-
- /** Deploy a list of system applications until they converge on the given version */
- private double upgradeAll(TARGET target, List<SystemApplication> applications) {
- int attempts = 0;
- int failures = 0;
- // Invert zone order if we're downgrading
- UpgradePolicy policy = target.downgrade() ? upgradePolicy.inverted() : upgradePolicy;
- for (UpgradePolicy.Step step : policy.steps()) {
- boolean converged = true;
- for (ZoneApi zone : step.zones()) {
- try {
- attempts++;
- converged &= upgradeAll(target, applications, zone, step.nodeSlice());
- } catch (UnreachableNodeRepositoryException e) {
- failures++;
- converged = false;
- log.warning(Text.format("%s: Failed to communicate with node repository in %s, continuing with next parallel zone: %s",
- this, zone, Exceptions.toMessageString(e)));
- } catch (Exception e) {
- failures++;
- converged = false;
- log.warning(Text.format("%s: Failed to upgrade zone: %s, continuing with next parallel zone: %s",
- this, zone, Exceptions.toMessageString(e)));
- }
- }
- if (!converged) {
- break;
- }
- }
- return asSuccessFactorDeviation(attempts, failures);
- }
-
- /** Returns whether all applications have converged to the target version in zone */
- private boolean upgradeAll(TARGET target, List<SystemApplication> applications, ZoneApi zone, NodeSlice nodeSlice) {
- Map<SystemApplication, Set<SystemApplication>> dependenciesByApplication = new HashMap<>();
- if (target.downgrade()) { // Invert dependencies when we're downgrading
- for (var application : applications) {
- dependenciesByApplication.computeIfAbsent(application, k -> new HashSet<>());
- for (var dependency : application.dependencies()) {
- dependenciesByApplication.computeIfAbsent(dependency, k -> new HashSet<>())
- .add(application);
- }
- }
- } else {
- applications.forEach(app -> dependenciesByApplication.put(app, Set.copyOf(app.dependencies())));
- }
- boolean converged = true;
- for (var kv : dependenciesByApplication.entrySet()) {
- SystemApplication application = kv.getKey();
- Set<SystemApplication> dependencies = kv.getValue();
- boolean allConverged = dependencies.stream().allMatch(app -> convergedOn(target, app, zone, nodeSlice));
- if (allConverged) {
- if (changeTargetTo(target, application, zone)) {
- upgrade(target, application, zone);
- }
- converged &= convergedOn(target, application, zone, nodeSlice);
- }
- }
- return converged;
- }
-
- /** Returns whether target version for application in zone should be changed */
- protected abstract boolean changeTargetTo(TARGET target, SystemApplication application, ZoneApi zone);
-
- /** Upgrade component to target version. Implementation should be idempotent */
- protected abstract void upgrade(TARGET target, SystemApplication application, ZoneApi zone);
-
- /** Returns whether application has converged to target version in zone */
- protected abstract boolean convergedOn(TARGET target, SystemApplication application, ZoneApi zone, NodeSlice nodeSlice);
-
- /** Returns the version target for the component upgraded by this, if any */
- protected abstract Optional<TARGET> target();
-
- /** Returns whether the upgrader should expect given node to upgrade */
- protected abstract boolean expectUpgradeOf(Node node, SystemApplication application, ZoneApi zone);
-
- /**
- * Find the version currently used by a slice of nodes, in given zone. If no such slice exists,
- * the lowest (or highest, when downgrading) overall version is returned.
- */
- protected final Optional<Version> versionOf(NodeSlice nodeSlice, ZoneApi zone, SystemApplication application,
- Function<Node, Version> versionField, boolean downgrading) {
- try {
- Map<Version, Long> nodeCountByVersion = controller().serviceRegistry().configServer()
- .nodeRepository()
- .list(zone.getVirtualId(), NodeFilter.all().applications(application.id()))
- .stream()
- .filter(node -> expectUpgradeOf(node, application, zone))
- .collect(Collectors.groupingBy(versionField,
- Collectors.counting()));
- long totalNodes = nodeCountByVersion.values().stream().reduce(Long::sum).orElse(0L);
- Set<Version> versionsOfMatchingSlices = new HashSet<>();
- for (var kv : nodeCountByVersion.entrySet()) {
- long nodesOnVersion = kv.getValue();
- if (nodeSlice.satisfiedBy(nodesOnVersion, totalNodes)) {
- versionsOfMatchingSlices.add(kv.getKey());
- }
- }
- if (!versionsOfMatchingSlices.isEmpty()) {
- return downgrading
- ? versionsOfMatchingSlices.stream().min(Comparator.naturalOrder())
- : versionsOfMatchingSlices.stream().max(Comparator.naturalOrder());
- }
- return downgrading
- ? nodeCountByVersion.keySet().stream().max(Comparator.naturalOrder())
- : nodeCountByVersion.keySet().stream().min(Comparator.naturalOrder());
- } catch (Exception e) {
- throw new UnreachableNodeRepositoryException(Text.format("Failed to get version for %s in %s: %s",
- application.id(), zone,
- Exceptions.toMessageString(e)));
- }
- }
-
- private static class UnreachableNodeRepositoryException extends RuntimeException {
- private UnreachableNodeRepositoryException(String reason) {
- super(reason);
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java
deleted file mode 100644
index 0f482b1a015..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunner.java
+++ /dev/null
@@ -1,196 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import ai.vespa.metrics.ControllerMetrics;
-import com.yahoo.concurrent.DaemonThreadFactory;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.deployment.InternalStepRunner;
-import com.yahoo.vespa.hosted.controller.deployment.JobController;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-import com.yahoo.vespa.hosted.controller.deployment.StepRunner;
-
-import java.time.Duration;
-import java.util.Map;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * Advances the set of {@link Run}s for a {@link JobController}.
- *
- * @author jonmv
- */
-public class JobRunner extends ControllerMaintainer {
-
- public static final Duration jobTimeout = Duration.ofDays(1).plusHours(1);
- private static final Logger log = Logger.getLogger(JobRunner.class.getName());
-
- private final JobController jobs;
- private final ExecutorService executors;
- private final StepRunner runner;
- private final Metrics metrics;
-
- public JobRunner(Controller controller, Duration duration) {
- this(controller, duration, Executors.newFixedThreadPool(32, new DaemonThreadFactory("job-runner-")),
- new InternalStepRunner(controller));
- }
-
- public JobRunner(Controller controller, Duration duration, ExecutorService executors, StepRunner runner) {
- this(controller, duration, executors, runner, new Metrics(controller.metric(), Duration.ofMillis(100)));
- }
-
- JobRunner(Controller controller, Duration duration, ExecutorService executors, StepRunner runner, Metrics metrics) {
- super(controller, duration);
- this.jobs = controller.jobController();
- this.jobs.setRunner(this::advance);
- this.executors = executors;
- this.runner = runner;
- this.metrics = metrics;
- }
-
- @Override
- protected double maintain() {
- execute(() -> jobs.active().forEach(this::advance));
- jobs.collectGarbage();
- return 1.0;
- }
-
- @Override
- public void shutdown() {
- super.shutdown();
- metrics.shutdown();
- executors.shutdown();
- }
-
- @Override
- public void awaitShutdown() {
- super.awaitShutdown();
- try {
- if ( ! executors.awaitTermination(40, TimeUnit.SECONDS)) {
- executors.shutdownNow();
- if ( ! executors.awaitTermination(10, TimeUnit.SECONDS))
- throw new IllegalStateException("Failed shutting down " + JobRunner.class.getName());
- }
- }
- catch (InterruptedException e) {
- log.log(Level.WARNING, "Interrupted during shutdown of " + JobRunner.class.getName(), e);
- Thread.currentThread().interrupt();
- }
- }
-
- public void advance(Run run) {
- if ( ! jobs.isDisabled(run.id().job())) advance(run.id());
- }
-
- /** Advances each of the ready steps for the given run, or marks it as finished, and stashes it. Public for testing. */
- public void advance(RunId id) {
- jobs.locked(id, run -> {
- if ( ! run.hasFailed()
- && controller().clock().instant().isAfter(run.sleepUntil().orElse(run.start()).plus(jobTimeout)))
- execute(() -> {
- jobs.abort(run.id(), "job timeout of " + jobTimeout + " reached", false);
- advance(run.id());
- });
- else if (run.readySteps().isEmpty())
- execute(() -> finish(run.id()));
- else if (run.hasFailed() || run.sleepUntil().map(sleepUntil -> ! sleepUntil.isAfter(controller().clock().instant())).orElse(true))
- run.readySteps().forEach(step -> execute(() -> advance(run.id(), step)));
-
- return null;
- });
- }
-
- private void finish(RunId id) {
- try {
- jobs.finish(id);
- if ( ! id.type().environment().isManuallyDeployed())
- controller().applications().deploymentTrigger().notifyOfCompletion(id.application());
- }
- catch (TimeoutException e) {
- // One of the steps are still being run — that's ok, we'll try to finish the run again later.
- }
- catch (Exception e) {
- log.log(Level.WARNING, "Exception finishing " + id, e);
- }
- }
-
- /** Attempts to advance the status of the given step, for the given run. */
- private void advance(RunId id, Step step) {
- try {
- AtomicBoolean changed = new AtomicBoolean(false);
- jobs.locked(id.application(), id.type(), step, lockedStep -> {
- jobs.locked(id, run -> {
- if ( ! run.readySteps().contains(step)) {
- changed.set(true);
- return run; // Someone may have updated the run status, making this step obsolete, so we bail out.
- }
-
- if (run.stepInfo(lockedStep.get()).orElseThrow().startTime().isEmpty())
- run = run.with(controller().clock().instant(), lockedStep);
-
- return run;
- });
-
- if ( ! changed.get()) {
- runner.run(lockedStep, id).ifPresent(status -> {
- jobs.update(id, status, lockedStep);
- changed.set(true);
- });
- }
- });
- if (changed.get())
- jobs.active(id).ifPresent(this::advance);
- }
- catch (TimeoutException e) {
- // Something else is already advancing this step, or a prerequisite -- try again later!
- }
- catch (RuntimeException e) {
- log.log(Level.WARNING, "Exception attempting to advance " + step + " of " + id, e);
- }
- }
-
- private void execute(Runnable task) {
- metrics.queued.incrementAndGet();
- executors.execute(() -> {
- metrics.queued.decrementAndGet();
- metrics.active.incrementAndGet();
- try { task.run(); }
- finally { metrics.active.decrementAndGet(); }
- });
- }
-
- static class Metrics {
-
- private final AtomicInteger queued = new AtomicInteger();
- private final AtomicInteger active = new AtomicInteger();
- private final ScheduledExecutorService reporter = Executors.newSingleThreadScheduledExecutor(new DaemonThreadFactory("job-runner-metrics-"));
- private final Metric metric;
- private final Metric.Context context;
-
- Metrics(Metric metric, Duration interval) {
- this.metric = metric;
- this.context = metric.createContext(Map.of());
- reporter.scheduleAtFixedRate(this::report, interval.toMillis(), interval.toMillis(), TimeUnit.MILLISECONDS);
- }
-
- void report() {
- metric.set(ControllerMetrics.DEPLOYMENT_JOBS_QUEUED.baseName(), queued.get(), context);
- metric.set(ControllerMetrics.DEPLOYMENT_JOBS_ACTIVE.baseName(), active.get(), context);
- }
-
- void shutdown() {
- reporter.shutdown();
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MeteringMonitorMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MeteringMonitorMaintainer.java
deleted file mode 100644
index 396ec1ec6f9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MeteringMonitorMaintainer.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient;
-
-import java.time.Duration;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Set;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * Reports discrepancies between currently deployed applications and
- * recently stored metering data in ResourceDatabaseClient.
- *
- * @author olaa
- */
-public class MeteringMonitorMaintainer extends ControllerMaintainer {
-
- private final ResourceDatabaseClient resourceDatabaseClient;
- private final Metric metric;
-
- protected static final String METERING_AGE_METRIC_NAME = "metering.age.seconds";
- private static final Logger logger = Logger.getLogger(MeteringMonitorMaintainer.class.getName());
-
- public MeteringMonitorMaintainer(Controller controller, Duration interval, ResourceDatabaseClient resourceDatabaseClient, Metric metric) {
- super(controller, interval, null, SystemName.allOf(SystemName::isPublic));
- this.resourceDatabaseClient = resourceDatabaseClient;
- this.metric = metric;
- }
-
- @Override
- protected double maintain() {
- var activeDeployments = activeDeployments();
- var lastSnapshotTime = resourceDatabaseClient.getOldestSnapshotTimestamp(activeDeployments);
- var age = controller().clock().instant().getEpochSecond() - lastSnapshotTime.getEpochSecond();
- metric.set(METERING_AGE_METRIC_NAME, age, metric.createContext(Collections.emptyMap()));
- return 1;
- }
-
- private Set<DeploymentId> activeDeployments() {
- return controller().applications().asList()
- .stream()
- .flatMap(app -> app.instances().values().stream())
- .flatMap(this::toProdDeployments)
- .collect(Collectors.toSet());
- }
-
- private Stream<DeploymentId> toProdDeployments(Instance instance) {
- return instance.deployments()
- .keySet()
- .stream()
- .filter(deployment -> deployment.environment().isProduction())
- .map(deployment -> new DeploymentId(instance.id(), deployment));
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
deleted file mode 100644
index 6f070cbba84..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporter.java
+++ /dev/null
@@ -1,388 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import ai.vespa.metrics.ConfigServerMetrics;
-import ai.vespa.metrics.ControllerMetrics;
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.athenz.client.zms.ZmsClient;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLog;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatusList;
-import com.yahoo.vespa.hosted.controller.deployment.JobList;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationLock;
-import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-/**
- * This calculates and reports system-wide metrics based on data from a {@link Controller}.
- *
- * @author mortent
- * @author mpolden
- */
-public class MetricsReporter extends ControllerMaintainer {
-
- public static final String TENANT_METRIC = ControllerMetrics.BILLING_TENANTS.baseName();
- public static final String DEPLOYMENT_FAIL_METRIC = ControllerMetrics.DEPLOYMENT_FAILURE_PERCENTAGE.baseName();
- public static final String DEPLOYMENT_AVERAGE_DURATION = ControllerMetrics.DEPLOYMENT_AVERAGE_DURATION.baseName();
- public static final String DEPLOYMENT_FAILING_UPGRADES = ControllerMetrics.DEPLOYMENT_FAILING_UPGRADES.baseName();
- public static final String DEPLOYMENT_BUILD_AGE_SECONDS = ControllerMetrics.DEPLOYMENT_BUILD_AGE_SECONDS.baseName();
- public static final String DEPLOYMENT_WARNINGS = ControllerMetrics.DEPLOYMENT_WARNINGS.baseName();
- public static final String DEPLOYMENT_OVERDUE_UPGRADE = ControllerMetrics.DEPLOYMENT_OVERDUE_UPGRADE_SECONDS.baseName();
- public static final String OS_CHANGE_DURATION = ControllerMetrics.DEPLOYMENT_OS_CHANGE_DURATION.baseName();
- public static final String PLATFORM_CHANGE_DURATION = ControllerMetrics.DEPLOYMENT_PLATFORM_CHANGE_DURATION.baseName();
- public static final String OS_NODE_COUNT = ControllerMetrics.DEPLOYMENT_NODE_COUNT_BY_OS_VERSION.baseName();
- public static final String PLATFORM_NODE_COUNT = ControllerMetrics.DEPLOYMENT_NODE_COUNT_BY_PLATFORM_VERSION.baseName();
- public static final String BROKEN_SYSTEM_VERSION = ControllerMetrics.DEPLOYMENT_BROKEN_SYSTEM_VERSION.baseName();
- public static final String REMAINING_ROTATIONS = ControllerMetrics.REMAINING_ROTATIONS.baseName();
- public static final String NAME_SERVICE_REQUESTS_QUEUED = ControllerMetrics.DNS_QUEUED_REQUESTS.baseName();
- public static final String OPERATION_PREFIX = "operation.";
- public static final String ZMS_QUOTA_USAGE = ControllerMetrics.ZMS_QUOTA_USAGE.baseName();
-
- private final Metric metric;
- private final Clock clock;
- private final ZmsClient zmsClient;
-
- // Keep track of reported node counts for each version
- private final ConcurrentHashMap<NodeCountKey, Long> nodeCounts = new ConcurrentHashMap<>();
-
- public MetricsReporter(Controller controller, Metric metric, ZmsClient zmsClient) {
- super(controller, Duration.ofMinutes(1)); // use fixed rate for metrics
- this.metric = metric;
- this.clock = controller.clock();
- this.zmsClient = zmsClient;
- }
-
- @Override
- public double maintain() {
- reportDeploymentMetrics();
- reportRemainingRotations();
- reportQueuedNameServiceRequests();
- VersionStatus versionStatus = controller().readVersionStatus();
- reportInfrastructureUpgradeMetrics(versionStatus);
- reportAuditLog();
- reportBrokenSystemVersion(versionStatus);
- reportTenantMetrics();
- reportZmsQuotaMetrics();
- return 0.0;
- }
-
- private void reportBrokenSystemVersion(VersionStatus versionStatus) {
- Version systemVersion = controller().systemVersion(versionStatus);
- VespaVersion.Confidence confidence = versionStatus.version(systemVersion).confidence();
- int isBroken = confidence == VespaVersion.Confidence.broken ? 1 : 0;
- metric.set(BROKEN_SYSTEM_VERSION, isBroken, metric.createContext(Map.of()));
- }
-
- private void reportAuditLog() {
- AuditLog log = controller().auditLogger().readLog();
- HashMap<String, HashMap<String, Integer>> metricCounts = new HashMap<>();
-
- for (AuditLog.Entry entry : log.entries()) {
- String[] resource = entry.resource().split("/");
- if((resource.length > 1) && (resource[1] != null)) {
- String api = resource[1];
- String operationMetric = OPERATION_PREFIX + api;
- HashMap<String, Integer> dimension = metricCounts.get(operationMetric);
- if (dimension != null) {
- Integer count = dimension.get(entry.principal());
- if (count != null) {
- dimension.replace(entry.principal(), ++count);
- } else {
- dimension.put(entry.principal(), 1);
- }
-
- } else {
- dimension = new HashMap<>();
- dimension.put(entry.principal(),1);
- metricCounts.put(operationMetric, dimension);
- }
- }
- }
- for (String operationMetric : metricCounts.keySet()) {
- for (String userDimension : metricCounts.get(operationMetric).keySet()) {
- metric.set(operationMetric, (metricCounts.get(operationMetric)).get(userDimension), metric.createContext(Map.of("operator", userDimension)));
- }
- }
- }
-
- private void reportInfrastructureUpgradeMetrics(VersionStatus versionStatus) {
- Map<NodeVersion, Duration> osChangeDurations = osChangeDurations();
- Map<NodeVersion, Duration> platformChangeDurations = platformChangeDurations(versionStatus);
- reportChangeDurations(osChangeDurations, OS_CHANGE_DURATION);
- reportChangeDurations(platformChangeDurations, PLATFORM_CHANGE_DURATION);
- reportNodeCount(osChangeDurations.keySet(), OS_NODE_COUNT);
- reportNodeCount(platformChangeDurations.keySet(), PLATFORM_NODE_COUNT);
- }
-
- private void reportRemainingRotations() {
- try (RotationLock lock = controller().routing().rotations().lock()) {
- int availableRotations = controller().routing().rotations().availableRotations(lock).size();
- metric.set(REMAINING_ROTATIONS, availableRotations, metric.createContext(Map.of()));
- }
- }
-
- private void reportDeploymentMetrics() {
- ApplicationList applications = ApplicationList.from(controller().applications().readable())
- .withProductionDeployment();
- DeploymentStatusList deployments = controller().jobController().deploymentStatuses(applications);
-
- metric.set(DEPLOYMENT_FAIL_METRIC, deploymentFailRatio(deployments) * 100, metric.createContext(Map.of()));
-
- averageDeploymentDurations(deployments, clock.instant()).forEach((instance, duration) -> {
- metric.set(DEPLOYMENT_AVERAGE_DURATION, duration.toSeconds(), metric.createContext(dimensions(instance)));
- });
-
- deploymentsFailingUpgrade(deployments).forEach((instance, failingJobs) -> {
- metric.set(DEPLOYMENT_FAILING_UPGRADES, failingJobs, metric.createContext(dimensions(instance)));
- });
-
- deploymentWarnings(deployments).forEach((instance, warnings) -> {
- metric.set(DEPLOYMENT_WARNINGS, warnings, metric.createContext(dimensions(instance)));
- });
-
- overdueUpgradeDurationByInstance(deployments).forEach((instance, overduePeriod) -> {
- metric.set(DEPLOYMENT_OVERDUE_UPGRADE, overduePeriod.toSeconds(), metric.createContext(dimensions(instance)));
- });
-
- for (Application application : applications.asList())
- application.revisions().last()
- .flatMap(ApplicationVersion::buildTime)
- .ifPresent(buildTime -> metric.set(DEPLOYMENT_BUILD_AGE_SECONDS,
- controller().clock().instant().getEpochSecond() - buildTime.getEpochSecond(),
- metric.createContext(dimensions(application.id().defaultInstance()))));
- }
-
- private Map<ApplicationId, Duration> overdueUpgradeDurationByInstance(DeploymentStatusList deployments) {
- Instant now = clock.instant();
- Map<ApplicationId, Duration> overdueUpgrades = new HashMap<>();
- for (var deploymentStatus : deployments) {
- for (var kv : deploymentStatus.instanceJobs().entrySet()) {
- ApplicationId instance = kv.getKey();
- JobList jobs = kv.getValue();
- boolean upgradeRunning = !jobs.production().upgrading().isEmpty();
- DeploymentInstanceSpec instanceSpec = deploymentStatus.application().deploymentSpec().requireInstance(instance.instance());
- Duration overdueDuration = upgradeRunning ? overdueUpgradeDuration(now, instanceSpec) : Duration.ZERO;
- overdueUpgrades.put(instance, overdueDuration);
- }
- }
- return Collections.unmodifiableMap(overdueUpgrades);
- }
-
- /** Returns how long an upgrade has been running inside a block window */
- static Duration overdueUpgradeDuration(Instant upgradingAt, DeploymentInstanceSpec instanceSpec) {
- Optional<Instant> lastOpened = Optional.empty(); // When the upgrade window most recently opened
- Instant oneWeekAgo = upgradingAt.minus(Duration.ofDays(7));
- Duration step = Duration.ofHours(1);
- for (Instant instant = upgradingAt.truncatedTo(ChronoUnit.HOURS); !instanceSpec.canUpgradeAt(instant); instant = instant.minus(step)) {
- if (!instant.isAfter(oneWeekAgo)) { // Wrapped around, the entire week is being blocked
- lastOpened = Optional.empty();
- break;
- }
- lastOpened = Optional.of(instant);
- }
- if (lastOpened.isEmpty()) return Duration.ZERO;
- return Duration.between(lastOpened.get(), upgradingAt);
- }
-
- private void reportQueuedNameServiceRequests() {
- metric.set(NAME_SERVICE_REQUESTS_QUEUED, controller().curator().readNameServiceQueue().requests().size(),
- metric.createContext(Map.of()));
- }
-
- private void reportNodeCount(Set<NodeVersion> nodeVersions, String metricName) {
- Map<NodeCountKey, Long> newNodeCounts = nodeVersions.stream()
- .collect(Collectors.groupingBy(nodeVersion -> {
- return new NodeCountKey(metricName,
- nodeVersion.currentVersion(),
- nodeVersion.zone());
- }, Collectors.counting()));
- nodeCounts.putAll(newNodeCounts);
- nodeCounts.forEach((key, count) -> {
- if (newNodeCounts.containsKey(key)) {
- // Version is still present: Update the metric.
- metric.set(metricName, count, metric.createContext(dimensions(key.zone, key.version)));
- } else if (key.metricName.equals(metricName)) {
- // Version is no longer present, but has been previously reported: Set it to zero.
- metric.set(metricName, 0, metric.createContext(dimensions(key.zone, key.version)));
- }
- });
- }
-
- private void reportChangeDurations(Map<NodeVersion, Duration> changeDurations, String metricName) {
- changeDurations.forEach((nodeVersion, duration) -> {
- metric.set(metricName, duration.toSeconds(), metric.createContext(dimensions(nodeVersion.hostname(), nodeVersion.zone())));
- });
- }
-
- private void reportTenantMetrics() {
- if (! controller().system().isPublic()) return;
-
- var planCounter = new TreeMap<String, Integer>();
-
- controller().tenants().asList().forEach(tenant -> {
- var planId = controller().serviceRegistry().billingController().getPlan(tenant.name());
- planCounter.merge(planId.value(), 1, Integer::sum);
- });
-
- planCounter.forEach((planId, count) -> {
- var context = metric.createContext(Map.of("plan", planId));
- metric.set(TENANT_METRIC, count, context);
- });
- }
-
- private void reportZmsQuotaMetrics() {
- var quota = zmsClient.getQuotaUsage();
- reportZmsQuota("subdomains", quota.getSubdomainUsage());
- reportZmsQuota("services", quota.getServiceUsage());
- reportZmsQuota("policies", quota.getPolicyUsage());
- reportZmsQuota("roles", quota.getRoleUsage());
- reportZmsQuota("groups", quota.getGroupUsage());
- }
-
- private void reportZmsQuota(String resourceType, double usage) {
- var context = metric.createContext(Map.of("resourceType", resourceType));
- metric.set(ZMS_QUOTA_USAGE, usage, context);
- }
-
- private Map<NodeVersion, Duration> platformChangeDurations(VersionStatus versionStatus) {
- return changeDurations(versionStatus.versions(), VespaVersion::nodeVersions);
- }
-
- private Map<NodeVersion, Duration> osChangeDurations() {
- return changeDurations(controller().os().status().versions().values(), Function.identity());
- }
-
- private <V> Map<NodeVersion, Duration> changeDurations(Collection<V> versions, Function<V, List<NodeVersion>> versionsGetter) {
- var now = clock.instant();
- var durations = new HashMap<NodeVersion, Duration>();
- for (var version : versions) {
- for (var nodeVersion : versionsGetter.apply(version)) {
- durations.put(nodeVersion, nodeVersion.changeDuration(now));
- }
- }
- return durations;
- }
-
- private static double deploymentFailRatio(DeploymentStatusList statuses) {
- return statuses.asList().stream()
- .mapToInt(status -> status.hasFailures() ? 1 : 0)
- .average().orElse(0);
- }
-
- private static Map<ApplicationId, Duration> averageDeploymentDurations(DeploymentStatusList statuses, Instant now) {
- return statuses.asList().stream()
- .flatMap(status -> status.instanceJobs().entrySet().stream())
- .collect(Collectors.toUnmodifiableMap(entry -> entry.getKey(),
- entry -> averageDeploymentDuration(entry.getValue(), now)));
- }
-
- private static Map<ApplicationId, Integer> deploymentsFailingUpgrade(DeploymentStatusList statuses) {
- return statuses.asList().stream()
- .flatMap(status -> status.instanceJobs().entrySet().stream())
- .collect(Collectors.toUnmodifiableMap(entry -> entry.getKey(),
- entry -> deploymentsFailingUpgrade(entry.getValue())));
- }
-
- private static int deploymentsFailingUpgrade(JobList jobs) {
- return jobs.failingHard().not().failingApplicationChange().size();
- }
-
- private static Duration averageDeploymentDuration(JobList jobs, Instant now) {
- List<Duration> jobDurations = jobs.lastTriggered()
- .mapToList(run -> Duration.between(run.start(), run.end().orElse(now)));
- return jobDurations.stream()
- .reduce(Duration::plus)
- .map(totalDuration -> totalDuration.dividedBy(jobDurations.size()))
- .orElse(Duration.ZERO);
- }
-
- private static Map<ApplicationId, Integer> deploymentWarnings(DeploymentStatusList statuses) {
- return statuses.asList().stream()
- .flatMap(status -> status.application().instances().values().stream())
- .collect(Collectors.toMap(Instance::id, a -> maxWarningCountOf(a.deployments().values())));
- }
-
- private static int maxWarningCountOf(Collection<Deployment> deployments) {
- return deployments.stream()
- .map(Deployment::metrics)
- .map(DeploymentMetrics::warnings)
- .map(Map::values)
- .flatMap(Collection::stream)
- .max(Integer::compareTo)
- .orElse(0);
- }
-
- private static Map<String, String> dimensions(ApplicationId application) {
- return Map.of("tenantName", application.tenant().value(),
- "app", application.application().value() + "." + application.instance().value(),
- "applicationId", application.toFullString());
- }
-
- private static Map<String, String> dimensions(HostName hostname, ZoneId zone) {
- return Map.of("host", hostname.value(),
- "zone", zone.value());
- }
-
- private static Map<String, String> dimensions(ZoneId zone, Version currentVersion) {
- return Map.of("zone", zone.value(),
- "currentVersion", currentVersion.toFullString());
- }
-
- private static class NodeCountKey {
-
- private final String metricName;
- private final Version version;
- private final ZoneId zone;
-
- public NodeCountKey(String metricName, Version version, ZoneId zone) {
- this.metricName = metricName;
- this.version = version;
- this.zone = zone;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- NodeCountKey that = (NodeCountKey) o;
- return metricName.equals(that.metricName) &&
- version.equals(that.version) &&
- zone.equals(that.zone);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(metricName, version, zone);
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcher.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcher.java
deleted file mode 100644
index 3ee9650d4ca..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcher.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.logging.Level;
-
-/**
- * This dispatches requests from {@link NameServiceQueue} to a {@link NameService}. Successfully dispatched requests are
- * removed from the queue.
- *
- * @author mpolden
- */
-public class NameServiceDispatcher extends ControllerMaintainer {
-
- private final Clock clock;
- private final CuratorDb db;
- private final NameService nameService;
-
- NameServiceDispatcher(Controller controller, NameService nameService, Duration interval) {
- super(controller, interval);
- this.clock = controller.clock();
- this.db = controller.curator();
- this.nameService = nameService;
- }
-
- public NameServiceDispatcher(Controller controller, Duration interval) {
- this(controller, controller.serviceRegistry().nameService(), interval);
- }
-
- @Override
- protected double maintain() {
- // Dispatch 1 request per second on average. Note that this is not entirely accurate because a NameService
- // implementation may need to perform multiple API-specific requests to execute a single NameServiceRequest
- int requestCount = trueIntervalInSeconds();
- final NameServiceQueue initial;
- try (var lock = db.lockNameServiceQueue()) {
- initial = db.readNameServiceQueue();
- }
- if (initial.requests().isEmpty() || requestCount == 0) return 1.0;
-
- Instant instant = clock.instant();
- NameServiceQueue remaining = initial.dispatchTo(nameService, requestCount);
- NameServiceQueue dispatched = initial.without(remaining);
-
- if (!dispatched.requests().isEmpty()) {
- Level logLevel = controller().system().isCd() ? Level.INFO : Level.FINE;
- log.log(logLevel, () -> "Dispatched name service request(s) in " +
- Duration.between(instant, clock.instant()) +
- ": " + dispatched);
- }
-
- try (var lock = db.lockNameServiceQueue()) {
- db.writeNameServiceQueue(db.readNameServiceQueue().replace(initial, remaining));
- }
- return dispatched.requests().size() / (double) Math.min(requestCount, initial.requests().size());
- }
-
- /** The true interval at which this runs in this cluster */
- private int trueIntervalInSeconds() {
- return (int) interval().dividedBy(db.cluster().size()).getSeconds();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java
deleted file mode 100644
index a712c4f35d9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeScheduler.java
+++ /dev/null
@@ -1,251 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.OsRelease;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.DayOfWeek;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * Automatically schedule upgrades to the next OS version.
- *
- * @author mpolden
- */
-public class OsUpgradeScheduler extends ControllerMaintainer {
-
- private static final Logger LOG = Logger.getLogger(OsUpgradeScheduler.class.getName());
-
- public OsUpgradeScheduler(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- protected double maintain() {
- Instant now = controller().clock().instant();
- int attempts = 0;
- int failures = 0;
- for (var cloud : controller().clouds()) {
- Optional<Change> change = changeIn(cloud, now, false);
- if (change.isEmpty()) continue;
- try {
- attempts++;
- controller().os().upgradeTo(change.get().osVersion().version(), cloud, false, false);
- } catch (IllegalArgumentException e) {
- failures++;
- LOG.log(Level.WARNING, "Failed to schedule OS upgrade: " + Exceptions.toMessageString(e) +
- ". Retrying in " + interval());
- }
- }
- return asSuccessFactorDeviation(attempts, failures);
- }
-
- /**
- * Returns the next OS version change
- *
- * @param cloud The cloud where the change will be deployed
- * @param now Current time
- * @param future Whether to return a change that cannot be scheduled now
- */
- public Optional<Change> changeIn(CloudName cloud, Instant now, boolean future) {
- Optional<OsVersionTarget> currentTarget = controller().os().target(cloud);
- if (currentTarget.isEmpty()) return Optional.empty();
- if (upgradingToNewMajor(cloud)) return Optional.empty(); // Skip further upgrades until major version upgrade is complete
-
- Version currentVersion = currentTarget.get().version();
- Change change = releaseIn(cloud).change(currentVersion, now);
- if (!change.osVersion().version().isAfter(currentVersion)) return Optional.empty();
- if (!future && !change.scheduleAt(now)) return Optional.empty();
-
- boolean certified = certified(change);
- if (!future && !certified) return Optional.empty();
- return Optional.of(change.withCertified(certified));
- }
-
- private boolean certified(Change change) {
- boolean certified = controller().os().certified(change.osVersion());
- if (!certified) {
- LOG.log(Level.WARNING, "Want to schedule " + change + ", but this change is not certified for " +
- "the current system version");
- }
- return certified;
- }
-
- private boolean upgradingToNewMajor(CloudName cloud) {
- return controller().os().status().versionsIn(cloud).stream()
- .filter(version -> !version.isEmpty()) // Ignore empty/unknown versions
- .map(Version::getMajor)
- .distinct()
- .count() > 1;
- }
-
- private Release releaseIn(CloudName cloud) {
- boolean useTaggedRelease = controller().zoneRegistry().zones().all().dynamicallyProvisioned().in(cloud)
- .zones().isEmpty();
- if (useTaggedRelease) {
- return new TaggedRelease(controller().system(), cloud, controller().serviceRegistry().artifactRepository());
- }
- return new CalendarVersionedRelease(controller().system(), cloud);
- }
-
- private static boolean canTriggerAt(Instant instant, boolean isCd) {
- ZonedDateTime dateTime = instant.atZone(ZoneOffset.UTC);
- int hourOfDay = dateTime.getHour();
- int dayOfWeek = dateTime.getDayOfWeek().getValue();
- // Upgrade can only be scheduled between 07:00 (02:00 in CD systems) and 12:59 UTC, Monday-Thursday
- int startHour = isCd ? 2 : 7;
- return hourOfDay >= startHour && hourOfDay <= 12 && dayOfWeek < 5;
- }
-
- /** Returns the earliest time, at or after instant, an upgrade can be scheduled */
- private static Instant schedulingInstant(Instant instant, SystemName system) {
- ChronoUnit schedulingResolution = ChronoUnit.HOURS;
- while (!canTriggerAt(instant, system.isCd())) {
- instant = instant.truncatedTo(schedulingResolution)
- .plus(schedulingResolution.getDuration());
- }
- return instant;
- }
-
- /** Returns the remaining cool-down period relative to releaseAge */
- private static Duration remainingCooldownOf(Duration cooldown, Duration releaseAge) {
- return releaseAge.compareTo(cooldown) < 0 ? cooldown.minus(releaseAge) : Duration.ZERO;
- }
-
- private interface Release {
-
- /** The next available change of this release at given instant */
- Change change(Version currentVersion, Instant instant);
-
- }
-
- /** OS version change and the earliest time it can be scheduled */
- public record Change(OsVersion osVersion, Instant scheduleAt, boolean certified) {
-
- public Change {
- Objects.requireNonNull(osVersion);
- Objects.requireNonNull(scheduleAt);
- }
-
- public Change withCertified(boolean certified) {
- return new Change(osVersion, scheduleAt, certified);
- }
-
- /** Returns whether this can be scheduled at given instant */
- public boolean scheduleAt(Instant instant) {
- return !instant.isBefore(scheduleAt);
- }
-
- }
-
- /** OS release based on a tag */
- private record TaggedRelease(SystemName system, CloudName cloud, ArtifactRepository artifactRepository) implements Release {
-
- public TaggedRelease {
- Objects.requireNonNull(system);
- Objects.requireNonNull(cloud);
- Objects.requireNonNull(artifactRepository);
- }
-
- @Override
- public Change change(Version currentVersion, Instant instant) {
- OsRelease release = artifactRepository.osRelease(currentVersion.getMajor(), OsRelease.Tag.latest);
- Duration cooldown = remainingCooldownOf(cooldown(), release.age(instant));
- Instant scheduleAt = schedulingInstant(instant.plus(cooldown), system);
- return new Change(new OsVersion(release.version(), cloud), scheduleAt, false);
- }
-
- /** The cool-down period that must pass before a release can be used */
- private Duration cooldown() {
- return system.isCd() ? Duration.ofDays(1) : Duration.ZERO;
- }
-
- }
-
- /** OS release based on calendar-versioning */
- record CalendarVersionedRelease(SystemName system, CloudName cloud) implements Release {
-
- /** A fixed point in time which the release schedule is calculated from */
- private static final Instant START_OF_SCHEDULE = LocalDate.of(2022, 1, 1)
- .atStartOfDay()
- .toInstant(ZoneOffset.UTC);
-
- /** The approximate time that should elapse between versions */
- private static final Duration SCHEDULING_STEP = Duration.ofDays(60);
-
- /** The day of week new releases are published */
- private static final DayOfWeek RELEASE_DAY = DayOfWeek.TUESDAY;
-
- /** How far into release day we should wait before triggering. This is to give the new release some time to propagate */
- private static final Duration COOLDOWN = Duration.ofHours(6);
-
- public CalendarVersionedRelease {
- Objects.requireNonNull(system);
- }
-
- @Override
- public Change change(Version currentVersion, Instant instant) {
- CalendarVersion version = findVersion(instant, currentVersion);
- Instant predicted = instant;
- while (!version.version().isAfter(currentVersion)) {
- predicted = predicted.plus(Duration.ofDays(1));
- version = findVersion(predicted, currentVersion);
- }
- Duration cooldown = remainingCooldownOf(COOLDOWN, version.age(instant));
- Instant schedulingInstant = schedulingInstant(instant.plus(cooldown), system);
- return new Change(new OsVersion(version.version(), cloud), schedulingInstant, false);
- }
-
- /** Find the most recent version available according to the scheduling step, relative to now */
- static CalendarVersion findVersion(Instant now, Version currentVersion) {
- Instant candidate = START_OF_SCHEDULE;
- while (!candidate.plus(SCHEDULING_STEP).isAfter(now)) {
- candidate = candidate.plus(SCHEDULING_STEP);
- }
- LocalDate date = LocalDate.ofInstant(candidate, ZoneOffset.UTC);
- while (date.getDayOfWeek() != RELEASE_DAY) {
- date = date.minusDays(1);
- }
- return CalendarVersion.from(date, currentVersion);
- }
-
- record CalendarVersion(Version version, LocalDate date) {
-
- private static final DateTimeFormatter CALENDAR_VERSION_PATTERN = DateTimeFormatter.ofPattern("yyyyMMdd");
-
- private static CalendarVersion from(LocalDate date, Version currentVersion) {
- String qualifier = date.format(CALENDAR_VERSION_PATTERN);
- return new CalendarVersion(new Version(currentVersion.getMajor(),
- currentVersion.getMinor(),
- currentVersion.getMicro(),
- qualifier),
- date);
- }
-
- /** Returns the age of this at given instant */
- private Duration age(Instant instant) {
- return Duration.between(date.atStartOfDay().toInstant(ZoneOffset.UTC), instant);
- }
-
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgrader.java
deleted file mode 100644
index 25a0abbce90..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgrader.java
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.zone.NodeSlice;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
-
-import java.time.Duration;
-import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Logger;
-
-/**
- * Trigger OS upgrade of zones in the system, according to the current OS version target.
- *
- * Target OS version is set per cloud, and an instance of this exists per cloud in the system.
- *
- * {@link OsUpgradeScheduler} may update the target automatically in supported clouds.
- *
- * @author mpolden
- */
-public class OsUpgrader extends InfrastructureUpgrader<OsVersionTarget> {
-
- private static final Logger log = Logger.getLogger(OsUpgrader.class.getName());
-
- private static final Set<Node.State> upgradableNodeStates = Set.of(
- Node.State.ready,
- Node.State.active,
- Node.State.reserved
- );
-
- private final CloudName cloud;
-
- public OsUpgrader(Controller controller, Duration interval, CloudName cloud) {
- super(controller, interval, controller.zoneRegistry().osUpgradePolicy(cloud), SystemApplication.all(), name(cloud));
- this.cloud = cloud;
- }
-
- @Override
- protected void upgrade(OsVersionTarget target, SystemApplication application, ZoneApi zone) {
- log.info(Text.format((target.downgrade() ? "Downgrading" : "Upgrading") + " OS of %s to version %s in %s in cloud %s", application.id(),
- target.osVersion().version().toFullString(),
- zone.getVirtualId(), zone.getCloudName()));
- controller().serviceRegistry().configServer().nodeRepository().upgradeOs(zone.getVirtualId(), application.nodeType(),
- target.osVersion().version(),
- target.downgrade());
- }
-
- @Override
- protected boolean convergedOn(OsVersionTarget target, SystemApplication application, ZoneApi zone, NodeSlice nodeSlice) {
- Version currentVersion = versionOf(nodeSlice, zone, application, Node::currentOsVersion, target.downgrade()).orElse(target.version());
- return satisfiedBy(currentVersion, target);
- }
-
- @Override
- protected boolean expectUpgradeOf(Node node, SystemApplication application, ZoneApi zone) {
- return cloud.equals(zone.getCloudName()) && // Cloud is managed by this upgrader
- application.shouldUpgradeOs() && // Application should upgrade in this cloud
- canUpgrade(node, false);
- }
-
- @Override
- protected Optional<OsVersionTarget> target() {
- // Return target if we have nodes in this cloud on the wrong version, or if we're downgrading a zone which does
- // not support downgrading all nodes
- return controller().os().target(cloud)
- .filter(target -> (target.downgrade() && !downgradingSupported()) ||
- controller().os().status().nodesIn(cloud).stream()
- .anyMatch(node -> !satisfiedBy(node.currentVersion(), target)));
- }
-
- @Override
- protected boolean changeTargetTo(OsVersionTarget target, SystemApplication application, ZoneApi zone) {
- if (!application.shouldUpgradeOs()) return false;
- return controller().serviceRegistry().configServer().nodeRepository()
- .targetVersionsOf(zone.getVirtualId())
- .osVersion(application.nodeType())
- .map(currentVersion -> !currentVersion.equals(target.version()))
- .orElse(true);
- }
-
- private boolean satisfiedBy(Version version, OsVersionTarget target) {
- if (target.downgrade() && downgradingSupported()) {
- // When downgrading we want an exact version if the cloud supports downgrades
- return version.equals(target.osVersion().version());
- }
- // Otherwise, matching or later version is fine
- return !version.isBefore(target.osVersion().version());
- }
-
- private boolean downgradingSupported() {
- return !controller().zoneRegistry().zones().all().dynamicallyProvisioned().in(cloud).zones().isEmpty();
- }
-
- /** Returns whether node currently allows upgrades */
- public static boolean canUpgrade(Node node, boolean includeDeferring) {
- return (includeDeferring || !node.deferOsUpgrade()) && upgradableNodeStates.contains(node.state());
- }
-
- private static String name(CloudName cloud) {
- return capitalize(cloud.value()) + OsUpgrader.class.getSimpleName(); // Prefix maintainer name with cloud name
- }
-
- private static String capitalize(String s) {
- if (s.isEmpty()) {
- return s;
- }
- char firstLetter = Character.toUpperCase(s.charAt(0));
- if (s.length() > 1) {
- return firstLetter + s.substring(1).toLowerCase();
- }
- return String.valueOf(firstLetter);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdater.java
deleted file mode 100644
index 2fd92970bc9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdater.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.logging.Level;
-
-/**
- * @author mpolden
- */
-public class OsVersionStatusUpdater extends ControllerMaintainer {
-
- public OsVersionStatusUpdater(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- protected double maintain() {
- try {
- OsVersionStatus newStatus = OsVersionStatus.compute(controller());
- controller().os().updateStatus(newStatus);
- controller().os().removeStaleCertifications(newStatus);
- return 0.0;
- } catch (Exception e) {
- log.log(Level.WARNING, "Failed to compute OS version status: " + Exceptions.toMessageString(e) +
- ". Retrying in " + interval());
- }
- return 1.0;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java
deleted file mode 100644
index 6c414e44a96..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployer.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.logging.Logger;
-
-/**
- * Deploys application changes which have been postponed due to an ongoing upgrade, or a block window.
- *
- * @author bratseth
- */
-public class OutstandingChangeDeployer extends ControllerMaintainer {
-
- private static final Logger logger = Logger.getLogger(OutstandingChangeDeployer.class.getName());
-
- public OutstandingChangeDeployer(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- protected double maintain() {
- double ok = 0, total = 0;
- for (Application application : ApplicationList.from(controller().applications().readable())
- .withProjectId()
- .withJobs()
- .asList())
- try {
- ++total;
- controller().applications().deploymentTrigger().triggerNewRevision(application.id());
- ++ok;
- }
- catch (RuntimeException e) {
- logger.info("Failed triggering new revision for " + application + ": " + Exceptions.toMessageString(e));
- }
- return total > 0 ? ok / total : 1;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReadyJobsTrigger.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReadyJobsTrigger.java
deleted file mode 100644
index e35bb139142..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReadyJobsTrigger.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.TriggerResult;
-
-import java.time.Duration;
-
-/**
- * Trigger ready deployment jobs. This drives jobs through each application's deployment pipeline.
- *
- * @author bratseth
- */
-public class ReadyJobsTrigger extends ControllerMaintainer {
-
- public ReadyJobsTrigger(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- public double maintain() {
- TriggerResult result = controller().applications().deploymentTrigger().triggerReadyJobs();
- long total = result.triggered() + result.failed();
- return total == 0 ? 1 : (double) result.triggered() / (result.triggered() + result.failed());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReindexingTriggerer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReindexingTriggerer.java
deleted file mode 100644
index 0668f8c481c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ReindexingTriggerer.java
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.ZonedDateTime;
-import java.time.temporal.ChronoUnit;
-import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * Periodically triggers reindexing for all hosted Vespa applications.
- *
- * Since reindexing is meant to be a background effort, exactly when things are triggered is not critical,
- * and a hash of id of each deployment is used to spread triggering out across the reindexing period.
- * Only deployments within a window of opportunity of two maintainer periods are considered in each run.
- * Reindexing is triggered for a deployment if it was last triggered more than half a period ago, and
- * if no reindexing is currently ongoing. This means an application may skip reindexing during a period
- * if it happens to reindex, e.g., a particular document type in its window of opportunity. This is fine.
- *
- * @author jonmv
- */
-public class ReindexingTriggerer extends ControllerMaintainer {
-
- static final Duration reindexingPeriod = Duration.ofDays(91); // 13 weeks — four times a year.
- static final double speed = 0.2; // Careful reindexing, as this is supposed to be a background operation.
-
- private static final Logger log = Logger.getLogger(ReindexingTriggerer.class.getName());
-
- public ReindexingTriggerer(Controller controller, Duration duration) {
- super(controller, duration);
- }
-
- @Override
- protected double maintain() {
- try {
- Instant now = controller().clock().instant();
- for (Application application : controller().applications().asList())
- application.productionDeployments().forEach((name, deployments) -> {
- ApplicationId id = application.id().instance(name);
- for (Deployment deployment : deployments)
- if ( inWindowOfOpportunity(now, id, deployment.zone())
- && reindexingIsReady(controller().applications().applicationReindexing(id, deployment.zone()), now))
- controller().applications().reindex(id, deployment.zone(), List.of(), List.of(), true, speed,
- "bakground reindexing, to account for changes in built-in linguistics components");
- });
- return 0.0;
- }
- catch (RuntimeException e) {
- log.log(Level.WARNING, "Failed to trigger reindexing: " + Exceptions.toMessageString(e));
- return 1.0;
- }
- }
-
- static boolean inWindowOfOpportunity(Instant now, ApplicationId id, ZoneId zone) {
- long dayOfPeriodToTrigger = Math.floorMod((id.serializedForm() + zone.value()).hashCode(), 65); // 13 weeks a 5 week days.
- long weekOfPeriodToTrigger = dayOfPeriodToTrigger / 5;
- long dayOfWeekToTrigger = dayOfPeriodToTrigger % 5;
- long daysSinceFirstMondayAfterEpoch = Instant.EPOCH.plus(Duration.ofDays(4)).until(now, ChronoUnit.DAYS); // EPOCH was a Thursday.
- long weekOfPeriod = (daysSinceFirstMondayAfterEpoch / 7) % 13; // 7 days to a calendar week, 13 weeks to the period.
- long dayOfWeek = daysSinceFirstMondayAfterEpoch % 7;
- long hourOfTrondheimTime = ZonedDateTime.ofInstant(now, java.time.ZoneId.of("Europe/Oslo")).getHour();
-
- return weekOfPeriod == weekOfPeriodToTrigger
- && dayOfWeek == dayOfWeekToTrigger
- && 8 <= hourOfTrondheimTime && hourOfTrondheimTime < 12;
- }
-
- static boolean reindexingIsReady(ApplicationReindexing reindexing, Instant now) {
- return reindexing.clusters().values().stream().flatMap(cluster -> cluster.ready().values().stream())
- .allMatch(status -> status.readyAt().map(now.minus(reindexingPeriod.dividedBy(2))::isAfter).orElse(true)
- && (status.startedAt().isEmpty() || status.endedAt().isPresent()));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java
deleted file mode 100644
index 5cadd13309b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainer.java
+++ /dev/null
@@ -1,300 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import ai.vespa.metrics.ControllerMetrics;
-import com.yahoo.concurrent.UncheckedTimeoutException;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.ClusterResources;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceSnapshot;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Clock;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.TimeoutException;
-import java.util.function.Function;
-import java.util.logging.Level;
-import java.util.stream.Collector;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * Creates a {@link ResourceSnapshot} per application, which is then passed on to a MeteringClient
- *
- * @author olaa
- */
-public class ResourceMeterMaintainer extends ControllerMaintainer {
-
- /**
- * Checks if the node is in some state where it is in active use by the tenant,
- * and not transitioning out of use, in a failed state, etc.
- */
- private static final Set<Node.State> METERABLE_NODE_STATES = EnumSet.of(
- Node.State.reserved, // an application will soon use this node
- Node.State.active, // an application is currently using this node
- Node.State.inactive // an application is not using it, but it is reserved for being re-introduced or decommissioned
- );
-
- private final ApplicationController applications;
- private final NodeRepository nodeRepository;
- private final ResourceDatabaseClient resourceClient;
- private final CuratorDb curator;
- private final SystemName systemName;
- private final Metric metric;
- private final Clock clock;
-
- private static final String METERING_LAST_REPORTED = ControllerMetrics.METERING_LAST_REPORTED.baseName();
- private static final String METERING_TOTAL_REPORTED = ControllerMetrics.METERING_TOTAL_REPORTED.baseName();
- private static final int METERING_REFRESH_INTERVAL_SECONDS = 1800;
-
- @SuppressWarnings("WeakerAccess")
- public ResourceMeterMaintainer(Controller controller,
- Duration interval,
- Metric metric,
- ResourceDatabaseClient resourceClient) {
- super(controller, interval);
- this.applications = controller.applications();
- this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- this.resourceClient = resourceClient;
- this.curator = controller.curator();
- this.systemName = controller.serviceRegistry().zoneRegistry().system();
- this.metric = metric;
- this.clock = controller.clock();
- }
-
- @Override
- protected double maintain() {
- Collection<ResourceSnapshot> resourceSnapshots;
- try {
- resourceSnapshots = getAllResourceSnapshots();
- } catch (Exception e) {
- log.log(Level.WARNING, "Failed to collect resource snapshots. Retrying in " + interval() + ". Error: " +
- Exceptions.toMessageString(e));
- return 1.0;
- }
-
- if (systemName.isPublic()) reportResourceSnapshots(resourceSnapshots);
- if (systemName.isPublic()) reportAllScalingEvents();
- updateDeploymentCost(resourceSnapshots);
- return 0.0;
- }
-
- void updateDeploymentCost(Collection<ResourceSnapshot> resourceSnapshots) {
- resourceSnapshots.stream()
- .collect(Collectors.groupingBy(snapshot -> TenantAndApplicationId.from(snapshot.getApplicationId()),
- Collectors.groupingBy(snapshot -> snapshot.getApplicationId().instance())))
- .forEach(this::updateDeploymentCost);
- }
-
- private void updateDeploymentCost(TenantAndApplicationId tenantAndApplication, Map<InstanceName, List<ResourceSnapshot>> snapshotsByInstance) {
- try {
- applications.lockApplicationIfPresent(tenantAndApplication, locked -> {
- for (InstanceName instanceName : locked.get().instances().keySet()) {
- Map<ZoneId, Double> deploymentCosts = snapshotsByInstance.getOrDefault(instanceName, List.of()).stream()
- .collect(Collectors.toUnmodifiableMap(
- ResourceSnapshot::getZoneId,
- snapshot -> cost(snapshot.resources(), systemName),
- Double::sum));
- locked = locked.with(instanceName, i -> i.withDeploymentCosts(deploymentCosts));
- updateCostMetrics(tenantAndApplication.instance(instanceName), deploymentCosts);
- }
- applications.store(locked);
- });
- } catch (UncheckedTimeoutException ignored) {
- // Will be retried on next maintenance, avoid throwing so we can update the other apps instead
- }
- }
-
- private void reportResourceSnapshots(Collection<ResourceSnapshot> resourceSnapshots) {
- resourceClient.writeResourceSnapshots(resourceSnapshots);
-
- updateMeteringMetrics(resourceSnapshots);
-
- try (var lock = curator.lockMeteringRefreshTime()) {
- if (needsRefresh(curator.readMeteringRefreshTime())) {
- resourceClient.refreshMaterializedView();
- curator.writeMeteringRefreshTime(clock.millis());
- }
- } catch (TimeoutException ignored) {
- // If it's locked, it means we're currently refreshing
- }
- }
-
- private List<ResourceSnapshot> getAllResourceSnapshots() {
- return controller().zoneRegistry().zones()
- .reachable().zones().stream()
- .map(ZoneApi::getId)
- .map(zoneId -> createResourceSnapshotsFromNodes(zoneId, nodeRepository.list(zoneId, NodeFilter.all())))
- .flatMap(Collection::stream)
- .toList();
- }
-
- private Stream<Instance> mapApplicationToInstances(Application application) {
- return application.instances().values().stream();
- }
-
- private Stream<DeploymentId> mapInstanceToDeployments(Instance instance) {
- return instance.deployments().keySet().stream()
- .filter(zoneId -> !zoneId.environment().isTest())
- .map(zoneId -> new DeploymentId(instance.id(), zoneId));
- }
-
- private Stream<Map.Entry<ClusterId, List<Cluster.ScalingEvent>>> mapDeploymentToClusterScalingEvent(DeploymentId deploymentId) {
- try {
- // TODO: get Application from controller.applications().deploymentInfo()
- return nodeRepository.getApplication(deploymentId.zoneId(), deploymentId.applicationId())
- .clusters().entrySet().stream()
- .map(cluster -> Map.entry(new ClusterId(deploymentId, cluster.getKey()), cluster.getValue().scalingEvents()));
- } catch (Exception e) {
- log.info("Could not retrieve scaling events for " + deploymentId + ": " + e.getMessage());
- return Stream.empty();
- }
- }
-
- private void reportAllScalingEvents() {
- var clusters = controller().applications().asList().stream()
- .flatMap(this::mapApplicationToInstances)
- .flatMap(this::mapInstanceToDeployments)
- .flatMap(this::mapDeploymentToClusterScalingEvent)
- .collect(Collectors.toMap(
- Map.Entry::getKey,
- Map.Entry::getValue
- ));
-
- for (var cluster : clusters.entrySet()) {
- resourceClient.writeScalingEvents(cluster.getKey(), cluster.getValue());
- }
- }
-
- private Collection<ResourceSnapshot> createResourceSnapshotsFromNodes(ZoneId zoneId, List<Node> nodes) {
- return nodes.stream()
- .filter(this::unlessNodeOwnerIsSystemApplication)
- .filter(this::isNodeStateMeterable)
- .filter(this::isClusterTypeMeterable)
- .collect(groupSnapshots(zoneId))
- .values()
- .stream()
- .toList();
- }
-
- private boolean unlessNodeOwnerIsSystemApplication(Node node) {
- return node.owner()
- .map(owner -> !owner.tenant().equals(SystemApplication.TENANT))
- .orElse(false);
- }
-
- private boolean isNodeStateMeterable(Node node) {
- return METERABLE_NODE_STATES.contains(node.state());
- }
-
- private boolean isClusterTypeMeterable(Node node) {
- return node.clusterType() != Node.ClusterType.admin; // log servers and shared cluster controllers
- }
-
- private boolean needsRefresh(long lastRefreshTimestamp) {
- return clock.instant()
- .minusSeconds(METERING_REFRESH_INTERVAL_SECONDS)
- .isAfter(Instant.ofEpochMilli(lastRefreshTimestamp));
- }
-
- public static double cost(ClusterResources clusterResources, SystemName systemName) {
- var totalResources = clusterResources.nodeResources().multipliedBy(clusterResources.nodes());
- return cost(totalResources, systemName);
- }
-
- private static double cost(NodeResources resources, SystemName systemName) {
- // Divide cost by 3 in non-public zones to show approx. AWS equivalent cost
- double costDivisor = systemName.isPublic() ? 1.0 : 3.0;
- return Math.round(resources.cost() * 100.0 / costDivisor) / 100.0;
- }
-
- private void updateMeteringMetrics(Collection<ResourceSnapshot> resourceSnapshots) {
- metric.set(METERING_LAST_REPORTED, clock.millis() / 1000, metric.createContext(Collections.emptyMap()));
- // total metered resource usage, for alerting on drastic changes
- metric.set(METERING_TOTAL_REPORTED,
- resourceSnapshots.stream()
- .mapToDouble(r -> r.resources().vcpu() + r.resources().memoryGb() + r.resources().diskGb()).sum(),
- metric.createContext(Collections.emptyMap()));
-
- resourceSnapshots.forEach(snapshot -> {
- var context = getMetricContext(snapshot);
- metric.set("metering.vcpu", snapshot.resources().vcpu(), context);
- metric.set("metering.memoryGB", snapshot.resources().memoryGb(), context);
- metric.set("metering.diskGB", snapshot.resources().diskGb(), context);
- });
- }
-
- private void updateCostMetrics(ApplicationId applicationId, Map<ZoneId, Double> deploymentCost) {
- deploymentCost.forEach((zoneId, cost) -> {
- var context = getMetricContext(applicationId, zoneId);
- metric.set("metering.cost.hourly", cost, context);
- });
- }
-
- private Metric.Context getMetricContext(ApplicationId applicationId, ZoneId zoneId) {
- return metric.createContext(Map.of(
- "tenantName", applicationId.tenant().value(),
- "applicationId", applicationId.toFullString(),
- "zoneId", zoneId.value()
- ));
- }
-
- private Metric.Context getMetricContext(ResourceSnapshot snapshot) {
- return metric.createContext(Map.of(
- "tenantName", snapshot.getApplicationId().tenant().value(),
- "applicationId", snapshot.getApplicationId().toFullString(),
- "zoneId", snapshot.getZoneId().value(),
- "architecture", snapshot.resources().architecture()
- ));
- }
-
- private Collector<Node, ?, Map<ResourceKey, ResourceSnapshot>> groupSnapshots(ZoneId zoneId) {
- return Collectors.collectingAndThen(
- Collectors.groupingBy(
- (Node node) -> new ResourceKey(node.owner().get(), node.resources().architecture(), node.wantedVersion().getMajor(), node.cloudAccount()),
- Collectors.toList()),
- convertNodeListToResourceSnapshot(zoneId));
- }
-
- private Function<Map<ResourceKey, List<Node>>, Map<ResourceKey, ResourceSnapshot>> convertNodeListToResourceSnapshot(ZoneId zoneId) {
- return nodesByMajor -> {
- return nodesByMajor.entrySet().stream()
- .collect(Collectors.toMap(
- entry -> entry.getKey(),
- entry -> ResourceSnapshot.from(entry.getValue(), clock.instant(), zoneId)));
- };
- }
-
- private record ResourceKey(
- ApplicationId applicationId,
- NodeResources.Architecture architecture,
- int majorVersion,
- CloudAccount account) {}
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceTagMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceTagMaintainer.java
deleted file mode 100644
index a0c94c0b9a7..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceTagMaintainer.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import org.apache.hc.client5.http.ConnectTimeoutException;
-
-import java.time.Duration;
-import java.util.Map;
-import java.util.Optional;
-import java.util.logging.Level;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger.INFRASTRUCTURE_APPLICATION;
-
-/**
- * @author olaa
- */
-public class ResourceTagMaintainer extends ControllerMaintainer {
-
- private final ResourceTagger resourceTagger;
-
- public ResourceTagMaintainer(Controller controller, Duration interval, ResourceTagger resourceTagger) {
- super(controller, interval);
- this.resourceTagger = resourceTagger;
- }
-
- @Override
- public double maintain() {
- controller().zoneRegistry().zones()
- .reachable()
- .in(CloudName.AWS)
- .zones().forEach(zone -> {
- Map<HostName, ApplicationId> applicationOfHosts = getTenantOfParentHosts(zone.getId());
- int taggedResources = resourceTagger.tagResources(zone, applicationOfHosts);
- if (taggedResources > 0)
- log.log(Level.INFO, "Tagged " + taggedResources + " resources in " + zone.getId());
- });
- return 0.0;
- }
-
- private Map<HostName, ApplicationId> getTenantOfParentHosts(ZoneId zoneId) {
- try {
- return controller().serviceRegistry().configServer().nodeRepository()
- .list(zoneId, NodeFilter.all())
- .stream()
- .filter(node -> node.type().isHost())
- .collect(Collectors.toMap(
- Node::hostname,
- node -> ownerApplicationId(node.type(), node.exclusiveTo(), node.exclusiveToClusterType()),
- (node1, node2) -> node1
- ));
- } catch (Exception e) {
- if (e.getCause() instanceof ConnectTimeoutException) {
- // Usually transient - try again later
- log.warning("Unable to retrieve hosts from " + zoneId.value());
- return Map.of();
- }
- throw e;
- }
- }
-
- // Must be the same as CloudHostProvisioner::ownerApplicationId
- private static ApplicationId ownerApplicationId(NodeType hostType, Optional<ApplicationId> exclusiveTo, Optional<Node.ClusterType> exclusiveToClusterType) {
- if (hostType != NodeType.host) return INFRASTRUCTURE_APPLICATION;
- return exclusiveTo.orElseGet(() ->
- ApplicationId.from("hosted-vespa", "shared-host", exclusiveToClusterType.map(Node.ClusterType::name).orElse("default")));
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RetriggerMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RetriggerMaintainer.java
deleted file mode 100644
index 3cbd7b3e0e6..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/RetriggerMaintainer.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.deployment.RetriggerEntry;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Optional;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * Trigger any jobs that are marked for re-triggering to effectuate some other change, e.g. a change in access to a
- * deployment's nodes.
- *
- * @author tokle
- */
-public class RetriggerMaintainer extends ControllerMaintainer {
-
- private static final Logger logger = Logger.getLogger(RetriggerMaintainer.class.getName());
-
- public RetriggerMaintainer(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- protected double maintain() {
- try (var lock = controller().curator().lockDeploymentRetriggerQueue()) {
- List<RetriggerEntry> retriggerEntries = controller().curator().readRetriggerEntries();
-
- // Trigger all jobs that still need triggering and is not running
- retriggerEntries.stream()
- .filter(this::needsTrigger)
- .filter(entry -> readyToTrigger(entry.jobId()))
- .forEach(entry -> controller().applications().deploymentTrigger().reTrigger(entry.jobId().application(), entry.jobId().type(),
- "re-triggered by " + getClass().getSimpleName()));
-
- // Remove all jobs that has succeeded with the required job run and persist the list
- List<RetriggerEntry> remaining = retriggerEntries.stream()
- .filter(this::needsTrigger)
- .toList();
- controller().curator().writeRetriggerEntries(remaining);
- } catch (Exception e) {
- logger.log(Level.WARNING, "Exception while triggering jobs", e);
- return 1.0;
- }
- return 0.0;
- }
-
- /** Returns true if a job is ready to run, i.e. is currently not running */
- private boolean readyToTrigger(JobId jobId) {
- Optional<Run> existingRun = controller().jobController().active(jobId.application()).stream()
- .filter(run -> run.id().type().equals(jobId.type()))
- .findFirst();
- return existingRun.isEmpty();
- }
-
- /** Returns true of job needs triggering. I.e. the job has not run since the queue item was last run */
- private boolean needsTrigger(RetriggerEntry entry) {
- return controller().jobController().lastCompleted(entry.jobId())
- .filter(run -> run.id().number() < entry.requiredRun())
- .isPresent();
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgrader.java
deleted file mode 100644
index c31f81497e6..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgrader.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.zone.NodeSlice;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersionTarget;
-
-import java.time.Duration;
-import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Logger;
-
-/**
- * Maintenance job which upgrades system applications.
- *
- * @author mpolden
- */
-public class SystemUpgrader extends InfrastructureUpgrader<VespaVersionTarget> {
-
- private static final Logger log = Logger.getLogger(SystemUpgrader.class.getName());
-
- private static final Set<Node.State> upgradableNodeStates = Set.of(Node.State.active, Node.State.reserved);
-
- public SystemUpgrader(Controller controller, Duration interval) {
- super(controller, interval, controller.zoneRegistry().upgradePolicy(), SystemApplication.notController(), null);
- }
-
- @Override
- protected void upgrade(VespaVersionTarget target, SystemApplication application, ZoneApi zone) {
- log.info(Text.format("Deploying %s on %s in %s", application.id(), target, zone.getId()));
- controller().applications().deploy(application, zone.getId(), target.version(), target.downgrade());
- }
-
- @Override
- protected boolean convergedOn(VespaVersionTarget target, SystemApplication application, ZoneApi zone, NodeSlice nodeSlice) {
- Optional<Version> currentVersion = versionOf(nodeSlice, zone, application, Node::currentVersion, target.downgrade());
- // Skip application convergence check if there are no nodes belonging to the application in the zone
- if (currentVersion.isEmpty()) return true;
-
- return currentVersion.get().equals(target.version()) &&
- application.configConvergedIn(zone.getId(), controller(), Optional.of(target.version()));
- }
-
- @Override
- protected boolean expectUpgradeOf(Node node, SystemApplication application, ZoneApi zone) {
- return eligibleForUpgrade(node);
- }
-
- @Override
- protected Optional<VespaVersionTarget> target() {
- VersionStatus status = controller().readVersionStatus();
- Optional<VespaVersion> target = status.controllerVersion()
- .filter(version -> {
- Version systemVersion = status.systemVersion()
- .map(VespaVersion::versionNumber)
- .orElse(Version.emptyVersion);
- return version.versionNumber().isAfter(systemVersion);
- })
- .filter(version -> version.confidence() != VespaVersion.Confidence.broken);
- boolean downgrade = target.isPresent() && target.get().confidence() == VespaVersion.Confidence.aborted;
- if (downgrade) {
- target = status.systemVersion();
- }
- return target.map(VespaVersion::versionNumber)
- .map(version -> new VespaVersionTarget(version, downgrade));
- }
-
- @Override
- protected boolean changeTargetTo(VespaVersionTarget target, SystemApplication application, ZoneApi zone) {
- if (application.hasApplicationPackage()) {
- // For applications with package we do not have a zone-wide version target. This means that we must check
- // the wanted version of each node.
- boolean zoneHasSharedRouting = controller().zoneRegistry().routingMethod(zone.getId()).isShared();
- return versionOf(NodeSlice.ALL, zone, application, Node::wantedVersion, target.downgrade())
- .map(wantedVersion -> !wantedVersion.equals(target.version()))
- .orElse(zoneHasSharedRouting); // Always upgrade if zone uses shared routing, but has no nodes allocated yet
- }
- return controller().serviceRegistry().configServer().nodeRepository()
- .targetVersionsOf(zone.getId())
- .vespaVersion(application.nodeType())
- .map(wantedVersion -> !wantedVersion.equals(target.version()))
- .orElse(true); // Always set target if there are no nodes
- }
-
- /** Returns whether node in application should be upgraded by this */
- public static boolean eligibleForUpgrade(Node node) {
- return upgradableNodeStates.contains(node.state());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleCleanupMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleCleanupMaintainer.java
deleted file mode 100644
index 5539c62be98..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleCleanupMaintainer.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Duration;
-
-public class TenantRoleCleanupMaintainer extends ControllerMaintainer {
-
- public TenantRoleCleanupMaintainer(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- protected double maintain() {
- var roleService = controller().serviceRegistry().roleService();
-
- var deletedTenants = controller().tenants().asList(true).stream()
- .filter(tenant -> tenant.type() == Tenant.Type.deleted)
- .map(Tenant::name)
- .toList();
- roleService.cleanupRoles(deletedTenants);
-
- if (controller().system().isPublic()) {
- controller().serviceRegistry().tenantSecretService().cleanupSecretStores(deletedTenants);
- }
-
- return 0.0;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainer.java
deleted file mode 100644
index f76b7634c62..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainer.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Comparator;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-public class TenantRoleMaintainer extends ControllerMaintainer {
-
- private static final Logger log = Logger.getLogger(TenantRoleMaintainer.class.getName());
-
- public TenantRoleMaintainer(Controller controller, Duration tenantRoleMaintainer) {
- super(controller, tenantRoleMaintainer);
- }
-
- @Override
- protected double maintain() {
- var roleService = controller().serviceRegistry().roleService();
- var tenants = controller().tenants().asList().stream()
- .sorted(Comparator.comparing(Tenant::tenantRolesLastMaintained))
- .limit(5)
- .toList();
-
- double ok = 0, attempts = 0, total = 0;
- // Create separate athenz service for all tenants
- for (Tenant tenant : tenants) {
- ++attempts;
- try {
- roleService.createTenantRole(tenant);
- }
- catch (RuntimeException e) {
- log.log(Level.WARNING, "Failed to create role for " + tenant.name() + ": " + Exceptions.toMessageString(e));
- }
- ++ok;
- }
- total += attempts == 0 ? 1 : ok / attempts;
-
- total += roleService.maintainRoles(tenants.stream().map(Tenant::name).toList());
-
- // Update last maintained timestamp
- var updated = controller().clock().instant();
- for (Tenant tenant : tenants) controller().tenants().updateLastTenantRolesMaintained(tenant.name(), updated);
-
- return total * 0.5;
- }
-
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java
deleted file mode 100644
index dceb3921061..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/Upgrader.java
+++ /dev/null
@@ -1,211 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.InstanceList;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatusList;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.Set;
-import java.util.function.UnaryOperator;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.PLATFORM;
-
-/**
- * Maintenance job which schedules applications for Vespa version upgrade
- *
- * @author bratseth
- * @author mpolden
- */
-public class Upgrader extends ControllerMaintainer {
-
- private static final Logger log = Logger.getLogger(Upgrader.class.getName());
-
- private final CuratorDb curator;
- private final Random random;
-
- public Upgrader(Controller controller, Duration interval) {
- this(controller, interval, controller.random(false));
- }
-
- Upgrader(Controller controller, Duration interval, Random random) {
- super(controller, interval);
- this.curator = controller.curator();
- this.random = random;
- }
-
- /**
- * Schedule application upgrades. Note that this implementation must be idempotent.
- */
- @Override
- public double maintain() {
- // Determine target versions for each upgrade policy
- VersionStatus versionStatus = controller().readVersionStatus();
- cancelBrokenUpgrades(versionStatus);
-
- DeploymentStatusList deploymentStatuses = deploymentStatuses(versionStatus);
- for (UpgradePolicy policy : UpgradePolicy.values())
- updateTargets(versionStatus, deploymentStatuses, policy);
-
- return 0.0;
- }
-
- private DeploymentStatusList deploymentStatuses(VersionStatus versionStatus) {
- return controller().jobController().deploymentStatuses(ApplicationList.from(controller().applications().readable())
- .withProjectId()
- .withJobs(),
- versionStatus);
- }
-
- /** Returns a list of all production application instances, except those which are pinned, which we should not manipulate here. */
- private InstanceList instances(DeploymentStatusList deploymentStatuses) {
- return InstanceList.from(deploymentStatuses)
- .withDeclaredJobs()
- .shuffle(random)
- .byIncreasingDeployedVersion()
- .unpinned();
- }
-
- private void cancelBrokenUpgrades(VersionStatus versionStatus) {
- // Cancel upgrades to broken targets (let other ongoing upgrades complete to avoid starvation)
- InstanceList instances = instances(deploymentStatuses(controller().readVersionStatus()));
- for (VespaVersion version : versionStatus.versions()) {
- if (version.confidence() == Confidence.broken)
- cancelUpgradesOf(instances.upgradingTo(version.versionNumber()).not().with(UpgradePolicy.canary),
- version.versionNumber() + " is broken");
- }
- }
-
- private void updateTargets(VersionStatus versionStatus, DeploymentStatusList deploymentStatuses, UpgradePolicy policy) {
- InstanceList instances = instances(deploymentStatuses);
- InstanceList remaining = instances.with(policy);
- Instant failureThreshold = controller().clock().instant().minus(DeploymentTrigger.maxFailingRevisionTime);
- Set<ApplicationId> failingRevision = InstanceList.from(deploymentStatuses.failingApplicationChangeSince(failureThreshold)).asSet();
-
- List<Version> targetAndNewer = new ArrayList<>();
- UnaryOperator<InstanceList> cancellationCriterion = policy == UpgradePolicy.canary ? i -> i.not().upgradingTo(targetAndNewer)
- : i -> i.failing()
- .not().upgradingTo(targetAndNewer);
-
- Map<ApplicationId, Version> targets = new LinkedHashMap<>();
- for (Version version : DeploymentStatus.targetsForPolicy(versionStatus, controller().systemVersion(versionStatus), policy)) {
- targetAndNewer.add(version);
- InstanceList eligible = eligibleForVersion(remaining, version, versionStatus);
- InstanceList outdated = cancellationCriterion.apply(eligible);
- cancelUpgradesOf(outdated.upgrading(), "Upgrading to outdated versions");
-
- // Prefer the newest target for each instance.
- remaining = remaining.not().matching(eligible.asList()::contains)
- .not().hasCompleted(Change.of(version));
- for (ApplicationId id : outdated.and(eligible.not().upgrading()))
- targets.put(id, version);
- }
-
- int numberToUpgrade = policy == UpgradePolicy.canary ? instances.size() : numberOfApplicationsToUpgrade();
- for (ApplicationId id : instances.matching(targets.keySet()::contains)) {
- if (failingRevision.contains(id)) {
- log.log(Level.INFO, "Cancelling failing revision for " + id);
- controller().applications().deploymentTrigger().cancelChange(id, ChangesToCancel.APPLICATION);
- }
-
- if (controller().applications().requireInstance(id).change().isEmpty()) {
- log.log(Level.INFO, "Triggering upgrade to " + targets.get(id) + " for " + id);
- controller().applications().deploymentTrigger().forceChange(id, Change.of(targets.get(id)));
- --numberToUpgrade;
- }
- if (numberToUpgrade <= 0) break;
- }
- }
-
- private InstanceList eligibleForVersion(InstanceList instances, Version version, VersionStatus versionStatus) {
- Change change = Change.of(version);
- return instances.not().failingOn(version)
- .allowingMajorVersion(version.getMajor(), versionStatus)
- .compatibleWithPlatform(version, controller().applications()::versionCompatibility)
- .not().hasCompleted(change) // Avoid rescheduling change for instances without production steps.
- .onLowerVersionThan(version)
- .canUpgradeAt(version, controller().clock().instant());
- }
-
- private void cancelUpgradesOf(InstanceList instances, String reason) {
- instances = instances.unpinned();
- if (instances.isEmpty()) return;
- log.info("Cancelling upgrading of " + instances.asList() + " instances: " + reason);
- for (ApplicationId instance : instances.asList())
- controller().applications().deploymentTrigger().cancelChange(instance, PLATFORM);
- }
-
- /** Returns the number of applications to upgrade in this run */
- private int numberOfApplicationsToUpgrade() {
- return numberOfApplicationsToUpgrade(interval().dividedBy(Math.max(1, controller().curator().cluster().size())).toMillis(),
- controller().clock().millis(),
- upgradesPerMinute());
- }
-
- /** Returns the number of applications to upgrade in the interval containing now */
- static int numberOfApplicationsToUpgrade(long intervalMillis, long nowMillis, double upgradesPerMinute) {
- long intervalStart = Math.round(nowMillis / (double) intervalMillis) * intervalMillis;
- double upgradesPerMilli = upgradesPerMinute / 60_000;
- long upgradesAtStart = (long) (intervalStart * upgradesPerMilli);
- long upgradesAtEnd = (long) ((intervalStart + intervalMillis) * upgradesPerMilli);
- return (int) (upgradesAtEnd - upgradesAtStart);
- }
-
- /** Returns number of upgrades per minute */
- public double upgradesPerMinute() {
- return curator.readUpgradesPerMinute();
- }
-
- /** Sets the number of upgrades per minute */
- public void setUpgradesPerMinute(double n) {
- if (n < 0)
- throw new IllegalArgumentException("Upgrades per minute must be >= 0, got " + n);
- curator.writeUpgradesPerMinute(n);
- }
-
- /** Override confidence for given version. This will cause the computed confidence to be ignored */
- public void overrideConfidence(Version version, Confidence confidence) {
- if (confidence == Confidence.aborted && !version.isAfter(controller().readSystemVersion())) {
- throw new IllegalArgumentException("Cannot override confidence to " + confidence +
- " for version " + version.toFullString() +
- ": Version may be in use by applications");
- }
- try (Mutex lock = curator.lockConfidenceOverrides()) {
- Map<Version, Confidence> overrides = new LinkedHashMap<>(curator.readConfidenceOverrides());
- overrides.put(version, confidence);
- curator.writeConfidenceOverrides(overrides);
- }
- }
-
- /** Returns all confidence overrides */
- public Map<Version, Confidence> confidenceOverrides() {
- return curator.readConfidenceOverrides();
- }
-
- /** Remove confidence override for given version */
- public void removeConfidenceOverride(Version version) {
- controller().removeConfidenceOverride(version::equals);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainer.java
deleted file mode 100644
index 0f39ef7d0f0..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainer.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.user.RoleMaintainer;
-
-import java.time.Duration;
-import java.util.Optional;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-
-/**
- * Maintains user management resources.
- * For now, ensures there's no discrepnacy between expected tenant/application roles and auth0/athenz roles
- *
- * @author olaa
- */
-public class UserManagementMaintainer extends ControllerMaintainer {
-
- private final RoleMaintainer roleMaintainer;
- private static final Logger logger = Logger.getLogger(UserManagementMaintainer.class.getName());
-
- public UserManagementMaintainer(Controller controller, Duration interval, RoleMaintainer roleMaintainer) {
- super(controller, interval);
- this.roleMaintainer = roleMaintainer;
- }
-
- @Override
- protected double maintain() {
- var tenants = controller().tenants().asList();
- var applications = controller().applications().idList()
- .stream()
- .map(appId -> ApplicationId.from(appId.tenant(), appId.application(), InstanceName.defaultName()))
- .toList();
- roleMaintainer.deleteLeftoverRoles(tenants, applications);
-
- if (!controller().system().isPublic()) {
- roleMaintainer.tenantsToDelete(tenants)
- .forEach(tenant -> {
- logger.warning(tenant.name() + " has a non-existing Athenz domain. Deleting");
- controller().applications().asList(tenant.name())
- .forEach(application -> controller().applications().deleteApplication(application.id(), Optional.empty()));
- controller().tenants().delete(tenant.name(), Optional.empty(), false);
- });
- }
-
- return 0.0;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainer.java
deleted file mode 100644
index b0d7a0c47e9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainer.java
+++ /dev/null
@@ -1,399 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.jdisc.Metric;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest.Impact;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestClient;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction.State;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VcmrReport;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest.Status;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.ZonedDateTime;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Predicate;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- *
- * Maintains status and execution of Vespa CMRs.
- *
- * Currently, this retires all affected tenant hosts if zone capacity allows it.
- *
- * @author olaa
- */
-public class VcmrMaintainer extends ControllerMaintainer {
-
- private static final Logger LOG = Logger.getLogger(VcmrMaintainer.class.getName());
- private static final int DAYS_TO_RETIRE = 2;
- private static final Duration ALLOWED_POSTPONEMENT_TIME = Duration.ofDays(7);
- protected static final String TRACKED_CMRS_METRIC = "cmr.tracked";
-
- private final CuratorDb curator;
- private final NodeRepository nodeRepository;
- private final ChangeRequestClient changeRequestClient;
- private final SystemName system;
- private final Metric metric;
-
- public VcmrMaintainer(Controller controller, Duration interval, Metric metric) {
- super(controller, interval, null, SystemName.allOf(Predicate.not(SystemName::isPublic)));
- this.curator = controller.curator();
- this.nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- this.changeRequestClient = controller.serviceRegistry().changeRequestClient();
- this.system = controller.system();
- this.metric = metric;
- }
-
- @Override
- protected double maintain() {
- var changeRequests = curator.readChangeRequests()
- .stream()
- .filter(shouldUpdate()).toList();
-
- var nodesByZone = nodesByZone();
-
- changeRequests.forEach(changeRequest -> {
- var nodes = impactedNodes(nodesByZone, changeRequest);
- var nextActions = getNextActions(nodes, changeRequest);
- var status = getStatus(nextActions, changeRequest);
-
- try (var lock = curator.lockChangeRequests()) {
- // Read the vcmr again, in case the source status has been updated
- curator.readChangeRequest(changeRequest.getId())
- .ifPresent(vcmr -> {
- var updatedVcmr = vcmr.withActionPlan(nextActions)
- .withStatus(status);
- curator.writeChangeRequest(updatedVcmr);
- if (nodes.keySet().size() == 1)
- approveChangeRequest(updatedVcmr);
- });
- }
- });
- updateMetrics();
- return 0.0;
- }
-
- /**
- * Status is based on:
- * 1. Whether the source has reportedly closed the request
- * 2. Whether any host requires operator action
- * 3. Whether any host is pending/started/finished retirement
- */
- private Status getStatus(List<HostAction> nextActions, VespaChangeRequest changeRequest) {
- if (changeRequest.getChangeRequestSource().isClosed()) {
- return Status.COMPLETED;
- }
-
- var byActionState = nextActions.stream().collect(Collectors.groupingBy(HostAction::getState, Collectors.counting()));
-
- if (byActionState.getOrDefault(State.REQUIRES_OPERATOR_ACTION, 0L) > 0) {
- return Status.REQUIRES_OPERATOR_ACTION;
- }
-
- if (byActionState.getOrDefault(State.OUT_OF_SYNC, 0L) > 0) {
- return Status.OUT_OF_SYNC;
- }
-
- if (byActionState.getOrDefault(State.RETIRING, 0L) > 0) {
- return Status.IN_PROGRESS;
- }
-
- if (Set.of(State.RETIRED, State.NONE).containsAll(byActionState.keySet())) {
- return Status.READY;
- }
-
- if (byActionState.getOrDefault(State.PENDING_RETIREMENT, 0L) > 0) {
- return Status.PENDING_ACTION;
- }
-
- return Status.NOOP;
- }
-
- private List<HostAction> getNextActions(Map<ZoneId, List<Node>> nodesByZone, VespaChangeRequest changeRequest) {
- return nodesByZone.entrySet()
- .stream()
- .flatMap(entry -> {
- var zone = entry.getKey();
- var nodes = entry.getValue();
- if (nodes.isEmpty()) {
- return Stream.empty();
- }
- var spareCapacity = hasSpareCapacity(zone, nodes);
- var impactedProxyCount = nodes.stream()
- .filter(node -> node.type() == NodeType.proxy)
- .count();
- return nodes.stream().map(node -> nextAction(zone, node, changeRequest, spareCapacity, impactedProxyCount));
- }).toList();
-
- }
-
- // Get the superset of impacted hosts by looking at impacted switches
- private Map<ZoneId, List<Node>> impactedNodes(Map<ZoneId, List<Node>> nodesByZone, VespaChangeRequest changeRequest) {
- return nodesByZone.entrySet()
- .stream()
- .filter(entry -> entry.getValue().stream().anyMatch(isImpacted(changeRequest))) // Skip zones without impacted nodes
- .collect(Collectors.toMap(
- Map.Entry::getKey,
- entry -> entry.getValue().stream().filter(isImpacted(changeRequest)).toList()
- ));
- }
-
- private Optional<HostAction> getPreviousAction(Node node, VespaChangeRequest changeRequest) {
- return changeRequest.getHostActionPlan()
- .stream()
- .filter(hostAction -> hostAction.getHostname().equals(node.hostname().value()))
- .findFirst();
- }
-
- private HostAction nextAction(ZoneId zoneId, Node node, VespaChangeRequest changeRequest, boolean spareCapacity, long impactedProxyCount) {
- var hostAction = getPreviousAction(node, changeRequest)
- .orElse(new HostAction(node.hostname().value(), State.NONE, Instant.now()));
-
- if (changeRequest.getChangeRequestSource().isClosed()) {
- LOG.fine(() -> changeRequest.getChangeRequestSource().id() + " is closed, recycling " + node.hostname());
- recycleNode(zoneId, node, hostAction);
- removeReport(zoneId, changeRequest, node);
- return hostAction.withState(State.COMPLETE);
- }
-
- if (isLowImpact(changeRequest))
- return hostAction;
-
- if (shouldAddReport(node, changeRequest.getChangeRequestSource().id(), hostAction))
- addReport(zoneId, changeRequest, node);
-
- if (isOutOfSync(node, hostAction))
- return hostAction.withState(State.OUT_OF_SYNC);
-
- if (isPostponed(changeRequest, hostAction)) {
- LOG.fine(() -> changeRequest.getChangeRequestSource().id() + " is postponed, recycling " + node.hostname());
- recycleNode(zoneId, node, hostAction);
- return hostAction.withState(State.PENDING_RETIREMENT);
- }
-
- if (!spareCapacity) {
- return hostAction.withState(State.REQUIRES_OPERATOR_ACTION);
- }
-
- if (node.type() != NodeType.host) {
- if (node.type() == NodeType.proxy && impactedProxyCount == 1)
- return hostAction.withState(State.READY);
- return hostAction.withState(State.REQUIRES_OPERATOR_ACTION);
- }
-
- if (shouldRetire(changeRequest, hostAction)) {
- if (!wantToRetireRecursive(zoneId, node)) {
- LOG.info(Text.format("Retiring %s due to %s", node.hostname().value(), changeRequest.getChangeRequestSource().id()));
- // TODO: Remove try/catch once retirement is stabilized
- try {
- setWantToRetire(zoneId, node, true);
- } catch (Exception e) {
- LOG.warning("Failed to retire host " + node.hostname() + ": " + Exceptions.toMessageString(e));
- // Will retry next maintenance run
- return hostAction;
- }
- }
- return hostAction.withState(State.RETIRING);
- }
-
- if (hasRetired(node, hostAction)) {
- LOG.fine(() -> node.hostname() + " has retired");
- return hostAction.withState(State.RETIRED);
- }
-
- if (pendingRetirement(node, hostAction)) {
- LOG.fine(() -> node.hostname() + " is pending retirement");
- return hostAction.withState(State.PENDING_RETIREMENT);
- }
-
- if (isFailed(node)) {
- return hostAction.withState(State.NONE);
- }
-
- return hostAction;
- }
-
- // Determines if a host and all its children are retiring
- private boolean wantToRetireRecursive(ZoneId zoneId, Node node) {
- var children = nodeRepository.list(zoneId, NodeFilter.all().parentHostnames(node.hostname()));
- return node.wantToRetire() &&
- children.stream().allMatch(Node::wantToRetire);
- }
-
- // Dirty host iff the parked host was retired by this maintainer
- private void recycleNode(ZoneId zoneId, Node node, HostAction hostAction) {
- if (hostAction.getState() == State.RETIRED &&
- node.state() == Node.State.parked) {
- LOG.info("Setting " + node.hostname() + " to dirty");
- nodeRepository.setState(zoneId, Node.State.dirty, node.hostname().value());
- }
- if (hostAction.getState() == State.RETIRING && node.wantToRetire()) {
- try {
- setWantToRetire(zoneId, node, false);
- } catch (Exception ignored) {}
- }
- }
-
- private boolean isPostponed(VespaChangeRequest changeRequest, HostAction action) {
- return List.of(State.RETIRED, State.RETIRING).contains(action.getState()) &&
- changeRequest.getChangeRequestSource().plannedStartTime()
- .minus(ALLOWED_POSTPONEMENT_TIME)
- .isAfter(ZonedDateTime.now());
- }
-
- private boolean shouldRetire(VespaChangeRequest changeRequest, HostAction action) {
- return action.getState() == State.PENDING_RETIREMENT &&
- getRetirementStartTime(changeRequest.getChangeRequestSource().plannedStartTime())
- .isBefore(ZonedDateTime.now());
- }
-
- private boolean hasRetired(Node node, HostAction hostAction) {
- return List.of(State.RETIRING, State.REQUIRES_OPERATOR_ACTION).contains(hostAction.getState()) &&
- node.state() == Node.State.parked;
- }
-
- private boolean pendingRetirement(Node node, HostAction action) {
- return List.of(State.NONE, State.REQUIRES_OPERATOR_ACTION).contains(action.getState())
- && node.state() == Node.State.active;
- }
-
- private boolean shouldAddReport(Node node, String vcmrId, HostAction previousAction) {
- var vcmrReport = VcmrReport.fromReports(node.reports());
- var hasReport = vcmrReport.getVcmrs().stream().map(VcmrReport.Vcmr::id).anyMatch(id -> id.equals(vcmrId));
- // Don't add report if none exists and this is not initial assessment
- // Presumably removed manually by operator.
- if (!hasReport && previousAction.getState() != State.NONE)
- return false;
- return true;
- }
-
- // Determines if node state is unexpected based on previous action taken
- private boolean isOutOfSync(Node node, HostAction action) {
- return action.getState() == State.RETIRED && node.state() != Node.State.parked ||
- action.getState() == State.RETIRING && !node.wantToRetire();
- }
-
- private boolean isFailed(Node node) {
- return node.state() == Node.State.failed ||
- node.state() == Node.State.breakfixed;
- }
-
- private Map<ZoneId, List<Node>> nodesByZone() {
- return controller().zoneRegistry()
- .zones()
- .reachable()
- .in(Environment.prod)
- .ids()
- .stream()
- .collect(Collectors.toMap(
- zone -> zone,
- zone -> nodeRepository.list(zone, NodeFilter.all())
- ));
- }
-
- private Predicate<Node> isImpacted(VespaChangeRequest changeRequest) {
- return node -> changeRequest.getImpactedHosts().contains(node.hostname().value()) ||
- node.switchHostname()
- .map(switchHostname -> changeRequest.getImpactedSwitches().contains(switchHostname))
- .orElse(false);
- }
- private Predicate<VespaChangeRequest> shouldUpdate() {
- return changeRequest -> changeRequest.getStatus() != Status.COMPLETED;
- }
-
- private boolean isLowImpact(VespaChangeRequest changeRequest) {
- return !List.of(Impact.HIGH, Impact.VERY_HIGH)
- .contains(changeRequest.getImpact());
- }
-
- private boolean hasSpareCapacity(ZoneId zoneId, List<Node> nodes) {
- var tenantHosts = nodes.stream()
- .filter(node -> node.type() == NodeType.host)
- .map(Node::hostname)
- .toList();
-
- return tenantHosts.isEmpty() ||
- nodeRepository.isReplaceable(zoneId, tenantHosts);
- }
-
- private void setWantToRetire(ZoneId zoneId, Node node, boolean wantToRetire) {
- nodeRepository.retire(zoneId, node.hostname().value(), wantToRetire, false);
- }
-
- private void approveChangeRequest(VespaChangeRequest changeRequest) {
- if (!system.equals(SystemName.main))
- return;
- if (changeRequest.getStatus() == Status.REQUIRES_OPERATOR_ACTION)
- return;
- if (changeRequest.getApproval() != ChangeRequest.Approval.REQUESTED)
- return;
-
- LOG.info("Approving " + changeRequest.getChangeRequestSource().id());
- changeRequestClient.approveChangeRequest(changeRequest);
- }
-
- private void removeReport(ZoneId zoneId, VespaChangeRequest changeRequest, Node node) {
- var report = VcmrReport.fromReports(node.reports());
-
- if (report.removeVcmr(changeRequest.getChangeRequestSource().id())) {
- updateReport(zoneId, node, report);
- }
- }
-
- private void addReport(ZoneId zoneId, VespaChangeRequest changeRequest, Node node) {
- var report = VcmrReport.fromReports(node.reports());
-
- if (report.addVcmr(changeRequest.getChangeRequestSource())) {
- updateReport(zoneId, node, report);
- }
- }
-
- private void updateReport(ZoneId zoneId, Node node, VcmrReport report) {
- LOG.fine(() -> Text.format("Updating report for %s: %s", node.hostname(), report));
- nodeRepository.updateReports(zoneId, node.hostname().value(), report.toNodeReports());
- }
-
- // Calculate wanted retirement start time, ignoring weekends
- // protected for testing
- protected ZonedDateTime getRetirementStartTime(ZonedDateTime plannedStartTime) {
- var time = plannedStartTime;
- var days = 0;
- while (days < DAYS_TO_RETIRE) {
- time = time.minusDays(1);
- if (time.getDayOfWeek().getValue() < 6) days++;
- }
- return time;
- }
-
- private void updateMetrics() {
- var cmrsByStatus = curator.readChangeRequests()
- .stream()
- .collect(Collectors.groupingBy(VespaChangeRequest::getStatus));
-
- for (var status : Status.values()) {
- var count = cmrsByStatus.getOrDefault(status, List.of()).size();
- metric.set(TRACKED_CMRS_METRIC, count, metric.createContext(Map.of("status", status.name())));
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdater.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdater.java
deleted file mode 100644
index 721819522f5..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdater.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Duration;
-import java.util.logging.Level;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.aborted;
-import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.broken;
-import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.high;
-import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.legacy;
-import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.low;
-import static com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor.Confidence.normal;
-
-/**
- * This maintenance job periodically updates the version status.
- * Since the version status is expensive to compute and does not need to be perfectly up to date,
- * we do not want to recompute it each time it is accessed.
- *
- * @author bratseth
- */
-public class VersionStatusUpdater extends ControllerMaintainer {
-
- public VersionStatusUpdater(Controller controller, Duration interval) {
- super(controller, interval);
- }
-
- @Override
- protected double maintain() {
- try {
- VersionStatus newStatus = VersionStatus.compute(controller());
- controller().updateVersionStatus(newStatus);
- newStatus.systemVersion().ifPresent(version -> {
- controller().serviceRegistry().systemMonitor().reportSystemVersion(version.versionNumber(),
- convert(version.confidence()));
- });
- return 0.0;
- } catch (Exception e) {
- log.log(Level.WARNING, "Failed to compute version status: " + Exceptions.toMessageString(e) +
- ". Retrying in " + interval());
- }
- return 1.0;
- }
-
- static SystemMonitor.Confidence convert(VespaVersion.Confidence confidence) {
- return switch (confidence) {
- case aborted -> aborted;
- case broken -> broken;
- case low -> low;
- case legacy -> legacy;
- case normal -> normal;
- case high -> high;
- };
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/package-info.java
deleted file mode 100644
index f8eed5804bb..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/metric/ApplicationMetrics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/metric/ApplicationMetrics.java
deleted file mode 100644
index 79d196b07fa..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/metric/ApplicationMetrics.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.metric;
-
-/**
- * Application metrics aggregated across all deployments.
- *
- * @author bratseth
- */
-public class ApplicationMetrics {
-
- private final double queryServiceQuality;
- private final double writeServiceQuality;
-
- public ApplicationMetrics(double queryServiceQuality, double writeServiceQuality) {
- this.queryServiceQuality = queryServiceQuality;
- this.writeServiceQuality = writeServiceQuality;
- }
-
- /**
- * Returns the quality of service for queries as a number between 1 (perfect) and 0 (none)
- */
- public double queryServiceQuality() {
- return queryServiceQuality;
- }
-
- /**
- * Returns the quality of service for writes as a number between 1 (perfect) and 0 (none)
- */
- public double writeServiceQuality() {
- return writeServiceQuality;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/metric/CostCalculator.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/metric/CostCalculator.java
deleted file mode 100644
index 23b81ebcd34..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/metric/CostCalculator.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.metric;
-
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceAllocation;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Clock;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.yolean.Exceptions.uncheck;
-
-/**
- * @author ldalves
- */
-public class CostCalculator {
-
- private static final double SELF_HOSTED_DISCOUNT = .5;
-
- public static String resourceShareByPropertyToCsv(NodeRepository nodeRepository,
- Controller controller,
- Clock clock,
- Map<Property, ResourceAllocation> fixedAllocations) {
-
- var date = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.of("UTC")).format(clock.instant());
-
- // Group properties by tenant name
- Map<TenantName, Property> propertyByTenantName = controller.tenants().asList().stream()
- .filter(AthenzTenant.class::isInstance)
- .collect(Collectors.toMap(Tenant::name,
- tenant -> ((AthenzTenant) tenant).property()));
-
- // Sum up allocations
- Map<Property, ResourceAllocation> allocationByProperty = new HashMap<>();
- var nodes = controller.zoneRegistry().zones()
- .reachable().in(Environment.prod).in(CloudName.YAHOO).zones().stream()
- .flatMap(zone -> uncheck(() -> nodeRepository.list(zone.getId(), NodeFilter.all()).stream()))
- .filter(node -> node.owner().isPresent() && !node.owner().get().tenant().equals(SystemApplication.TENANT))
- .toList();
- var totalAllocation = ResourceAllocation.ZERO;
- for (var node : nodes) {
- Property property = propertyByTenantName.get(node.owner().get().tenant());
- if (property == null) continue;
- var allocation = allocationByProperty.getOrDefault(property, ResourceAllocation.ZERO);
- var nodeAllocation = new ResourceAllocation(node.resources().vcpu(), node.resources().memoryGb(), node.resources().diskGb(), node.resources().architecture());
- allocationByProperty.put(property, allocation.plus(nodeAllocation));
- totalAllocation = totalAllocation.plus(nodeAllocation);
- }
-
- // Add fixed allocations from config
- for (var kv : fixedAllocations.entrySet()) {
- var property = kv.getKey();
- var allocation = allocationByProperty.getOrDefault(property, ResourceAllocation.ZERO);
- var discountedFixedAllocation = kv.getValue().multiply(SELF_HOSTED_DISCOUNT);
- allocationByProperty.put(property, allocation.plus(discountedFixedAllocation));
- totalAllocation = totalAllocation.plus(discountedFixedAllocation);
- }
-
- return toCsv(allocationByProperty, date, totalAllocation);
- }
-
- private static String toCsv(Map<Property, ResourceAllocation> resourceShareByProperty, String date, ResourceAllocation totalResourceAllocation) {
- String header = "Date,Property,Reserved Cpu Cores,Reserved Memory GB,Reserved Disk Space GB,Usage Fraction\n";
- String entries = resourceShareByProperty.entrySet().stream()
- .sorted((Comparator.comparingDouble(entry -> entry.getValue().usageFraction(totalResourceAllocation))))
- .map(propertyEntry -> {
- ResourceAllocation r = propertyEntry.getValue();
- return Stream.of(date, propertyEntry.getKey(), r.getCpuCores(), r.getMemoryGb(), r.getDiskGb(), r.usageFraction(totalResourceAllocation))
- .map(Object::toString).collect(Collectors.joining(","));
- })
- .collect(Collectors.joining("\n"));
- return header + entries;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/FormattedNotification.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/FormattedNotification.java
deleted file mode 100644
index bed053d592f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/FormattedNotification.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-import java.util.Objects;
-
-/**
- * Contains formatted text that can be displayed to a user to give extra information and pointers for a given
- * Notification.
- *
- * @author enygaard
- */
-public record FormattedNotification(Notification notification, String prettyType, String messagePrefix, String uri) {
-
- public FormattedNotification {
- Objects.requireNonNull(prettyType);
- Objects.requireNonNull(messagePrefix);
- Objects.requireNonNull(uri);
- Objects.requireNonNull(notification);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java
deleted file mode 100644
index c54791f511e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MailTemplating.java
+++ /dev/null
@@ -1,103 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-import com.yahoo.yolean.Exceptions;
-import org.apache.velocity.VelocityContext;
-import org.apache.velocity.app.Velocity;
-import org.apache.velocity.app.VelocityEngine;
-import org.apache.velocity.runtime.resource.loader.StringResourceLoader;
-import org.apache.velocity.runtime.resource.util.StringResourceRepository;
-import org.apache.velocity.tools.generic.EscapeTool;
-
-import java.io.StringWriter;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * @author bjorncs
- */
-public class MailTemplating {
-
- public enum Template {
- MAIL("mail"), DEFAULT_MAIL_CONTENT("default-mail-content"), NOTIFICATION_MESSAGE("notification-message"),
- MAIL_VERIFICATION("mail-verification"), TRIAL_SIGNED_UP("trial-signed-up"), TRIAL_MIDWAY_CHECKIN("trial-midway-checkin"),
- TRIAL_EXPIRES_IMMEDIATELY("trial-expires-immediately"), TRIAL_EXPIRED("trial-expired")
- ;
-
- public static Optional<Template> fromId(String id) {
- return Arrays.stream(values()).filter(t -> t.id.equals(id)).findAny();
- }
-
- private final String id;
-
- Template(String id) { this.id = id; }
-
- public String getId() { return id; }
- }
-
- private final VelocityEngine velocity;
- private final EscapeTool escapeTool = new EscapeTool();
- private final ConsoleUrls consoleUrls;
-
- public MailTemplating(ConsoleUrls consoleUrls) {
- this.velocity = createTemplateEngine();
- this.consoleUrls = consoleUrls;
- }
-
- public String generateDefaultMailHtml(Template mailBodyTemplate, Map<String, Object> params, TenantName tenant) {
- var ctx = createVelocityContext();
- ctx.put("accountNotificationLink", consoleUrls.tenantNotifications(tenant));
- ctx.put("privacyPolicyLink", "https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html");
- ctx.put("termsOfServiceLink", consoleUrls.termsOfService());
- ctx.put("supportLink", consoleUrls.support());
- ctx.put("mailBodyTemplate", mailBodyTemplate.getId());
- params.forEach(ctx::put);
- return render(ctx, Template.MAIL);
- }
-
- public String generateMailVerificationHtml(PendingMailVerification pmf) {
- var ctx = createVelocityContext();
- ctx.put("verifyLink", consoleUrls.verifyEmail(pmf.getVerificationCode()));
- ctx.put("email", pmf.getMailAddress());
- return render(ctx, Template.MAIL_VERIFICATION);
- }
-
- public String escapeHtml(String s) { return escapeTool.html(s); }
-
- private VelocityContext createVelocityContext() {
- var ctx = new VelocityContext();
- ctx.put("esc", escapeTool);
- return ctx;
- }
-
- private String render(VelocityContext ctx, Template template) {
- var writer = new StringWriter();
- // Ignoring return value - implementation either returns 'true' or throws, never 'false'
- velocity.mergeTemplate(template.getId(), StandardCharsets.UTF_8.name(), ctx, writer);
- return writer.toString();
- }
-
- private static VelocityEngine createTemplateEngine() {
- var v = new VelocityEngine();
- v.setProperty(Velocity.RESOURCE_LOADERS, "string");
- v.setProperty(Velocity.RESOURCE_LOADER + ".string.class", StringResourceLoader.class.getName());
- v.setProperty(Velocity.RESOURCE_LOADER + ".string.repository.static", "false");
- v.init();
- var repo = (StringResourceRepository) v.getApplicationAttribute(StringResourceLoader.REPOSITORY_NAME_DEFAULT);
- Arrays.stream(Template.values()).forEach(t -> registerTemplate(repo, t.getId()));
- return v;
- }
-
- private static void registerTemplate(StringResourceRepository repo, String name) {
- var templateStr = Exceptions.uncheck(() -> {
- var in = MailTemplating.class.getResourceAsStream("/mail/%s.vm".formatted(name));
- return new String(in.readAllBytes());
- });
- repo.putStringResource(name, templateStr);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MissingOptionalException.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MissingOptionalException.java
deleted file mode 100644
index 50e4cd40af7..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/MissingOptionalException.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-/**
- * Used to signal that an expected value was not present when creating NotificationContent
- *
- * @author enygaard
- */
-class MissingOptionalException extends RuntimeException {
- private final String field;
- public MissingOptionalException(String field) {
- super(field + " was expected but not present");
- this.field = field;
- }
-
- public String field() {
- return field;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
deleted file mode 100644
index 897e0be2d22..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notification.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-import java.time.Instant;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-/**
- * Represents an event that we want to notify the tenant about. The message(s) should be short
- * and only describe event details: the final presentation will prefix the message with general
- * information from other metadata in this notification (e.g. links to relevant console views
- * and/or relevant documentation.
- *
- * @author freva
- */
-public record Notification(Instant at, Notification.Type type, Notification.Level level, NotificationSource source,
- String title, List<String> messages, Optional<MailContent> mailContent) {
-
- public Notification(Instant at, Type type, Level level, NotificationSource source, String title, List<String> messages) {
- this(at, type, level, source, title, messages, Optional.empty());
- }
-
- public Notification(Instant at, Type type, Level level, NotificationSource source, List<String> messages) {
- this(at, type, level, source, "", messages);
- }
-
- public Notification {
- Objects.requireNonNull(at, "at cannot be null");
- Objects.requireNonNull(type, "type cannot be null");
- Objects.requireNonNull(level, "level cannot be null");
- Objects.requireNonNull(source, "source cannot be null");
- Objects.requireNonNull(title, "title cannot be null");
- messages = List.copyOf(Objects.requireNonNull(messages, "messages cannot be null"));
-
- // Allowing empty title temporarily until all notifications have a title
- // if (title.isBlank()) throw new IllegalArgumentException("title cannot be empty");
- if (messages.isEmpty() && title.isBlank()) throw new IllegalArgumentException("messages cannot be empty when title is empty");
-
- Objects.requireNonNull(mailContent);
- }
-
- public enum Level {
- // Must be ordered in order of importance
- info, warning, error
- }
-
- public enum Type {
-
- /** Related to contents of application package, e.g., usage of deprecated features/syntax */
- applicationPackage,
-
- /** Related to contents of application package detectable by the controller on submission */
- submission,
-
- /** Related to contents of application test package, e.g., mismatch between deployment spec and provided tests */
- testPackage,
-
- /** Related to deployment of application, e.g., system test failure, node allocation failure, internal errors, etc. */
- deployment,
-
- /** Application cluster is (near) external feed blocked */
- feedBlock,
-
- /** Application cluster is reindexing document(s) */
- reindex,
-
- /** Account, e.g. expiration of trial plan */
- account,
- }
-
- public static class MailContent {
- private final MailTemplating.Template template;
- private final SortedMap<String, Object> values;
- private final String subject;
-
- private MailContent(Builder b) {
- template = Objects.requireNonNull(b.template);
- values = new TreeMap<>(b.values);
- subject = b.subject;
- }
-
- public MailTemplating.Template template() { return template; }
- public SortedMap<String, Object> values() { return Collections.unmodifiableSortedMap(values); }
- public Optional<String> subject() { return Optional.ofNullable(subject); }
-
- public static Builder fromTemplate(MailTemplating.Template template) { return new Builder(template); }
-
- public static class Builder {
- private final MailTemplating.Template template;
- private final Map<String, Object> values = new HashMap<>();
- private String subject;
-
- private Builder(MailTemplating.Template template) {
- this.template = template;
- }
-
- public Builder with(String name, String value) { values.put(name, value); return this; }
- public Builder with(String name, Collection<String> items) { values.put(name, List.copyOf(items)); return this; }
- public Builder subject(String s) { this.subject = s; return this; }
- public MailContent build() { return new MailContent(this); }
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- MailContent that = (MailContent) o;
- return Objects.equals(template, that.template) && Objects.equals(values, that.values) && Objects.equals(subject, that.subject);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(template, values, subject);
- }
-
- @Override
- public String toString() {
- return "MailContent{" +
- "template='" + template + '\'' +
- ", values=" + values +
- ", subject='" + subject + '\'' +
- '}';
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationFormatter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationFormatter.java
deleted file mode 100644
index e9b38f7a122..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationFormatter.java
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
-
-import java.util.Objects;
-import java.util.Optional;
-
-import static com.yahoo.vespa.hosted.controller.notification.Notifier.notificationLink;
-
-/**
- * Created a NotificationContent for a given Notification.
- *
- * The formatter will create specific summary, message start and URI for a given Notification.
- *
- * @author enygaard
- */
-public class NotificationFormatter {
- private final ConsoleUrls consoleUrls;
-
- public NotificationFormatter(ConsoleUrls consoleUrls) {
- this.consoleUrls = Objects.requireNonNull(consoleUrls);
- }
-
- public FormattedNotification format(Notification n) {
- return switch (n.type()) {
- case applicationPackage, submission -> applicationPackage(n);
- case deployment -> deployment(n);
- case testPackage -> testPackage(n);
- case reindex -> reindex(n);
- case feedBlock -> feedBlock(n);
- default -> new FormattedNotification(n, n.type().name(), "", consoleUrls.tenantOverview(n.source().tenant()));
- };
- }
-
- private FormattedNotification applicationPackage(Notification n) {
- var source = n.source();
- var application = requirePresent(source.application(), "application");
- var message = Text.format("Application package for %s%s has %s",
- application,
- source.instance().map(instance -> "." + instance.value()).orElse(""),
- levelText(n.level(), n.messages().size()));
- return new FormattedNotification(n, "Application package", message, notificationLink(consoleUrls, n.source()));
- }
-
- private FormattedNotification deployment(Notification n) {
- var source = n.source();
- var message = Text.format("%s for %s.%s has %s",
- jobText(source),
- requirePresent(source.application(), "application"),
- requirePresent(source.instance(), "instance"),
- levelText(n.level(), n.messages().size()));
- return new FormattedNotification(n,"Deployment", message, notificationLink(consoleUrls, n.source()));
- }
-
- private FormattedNotification testPackage(Notification n) {
- var source = n.source();
- var application = requirePresent(source.application(), "application");
- var message = Text.format("There %s with tests for %s%s",
- n.messages().size() > 1 ? "are problems" : "is a problem",
- application,
- source.instance().map(i -> "."+i).orElse(""));
- return new FormattedNotification(n, "Test package", message, notificationLink(consoleUrls, n.source()));
- }
-
- private FormattedNotification reindex(Notification n) {
- var message = Text.format("%s is reindexing", clusterInfo(n.source()));
- var application = requirePresent(n.source().application(), "application");
- var instance = requirePresent(n.source().instance(), "instance");
- var clusterId = requirePresent(n.source().clusterId(), "clusterId");
- var zone = requirePresent(n.source().zoneId(), "zoneId");
- return new FormattedNotification(n, "Reindex", message,
- consoleUrls.clusterReindexing(ApplicationId.from(n.source().tenant(), application, instance), zone, clusterId));
- }
-
- private FormattedNotification feedBlock(Notification n) {
- String type = n.level() == Notification.Level.warning ? "Nearly feed blocked" : "Feed blocked";
- var message = Text.format("%s is %s", clusterInfo(n.source()), type.toLowerCase());
- return new FormattedNotification(n, type, message, notificationLink(consoleUrls, n.source()));
- }
-
- private String jobText(NotificationSource source) {
- var jobType = requirePresent(source.jobType(), "jobType");
- var zone = jobType.zone();
- var runNumber = source.runNumber().orElseThrow(() -> new MissingOptionalException("runNumber"));
- switch (zone.environment().value()) {
- case "production":
- return Text.format("Deployment job #%d to %s", runNumber, zone.region());
- case "test":
- return Text.format("Test job #%d to %s", runNumber, zone.region());
- case "dev":
- case "perf":
- return Text.format("Deployment job #%d to %s.%s", runNumber, zone.environment().value(), zone.region().value());
- }
- switch (jobType.jobName()) {
- case "system-test":
- case "staging-test":
- }
- return Text.format("%s #%d", jobType.jobName(), runNumber);
- }
-
- private String levelText(Notification.Level level, int count) {
- return switch (level) {
- case error -> "failed";
- case warning -> count > 1 ? Text.format("%d warnings", count) : "a warning";
- default -> count > 1 ? Text.format("%d messages", count) : "a message";
- };
- }
-
- private String clusterInfo(NotificationSource source) {
- var application = requirePresent(source.application(), "application");
- var instance = requirePresent(source.instance(), "instance");
- var zone = requirePresent(source.zoneId(), "zoneId");
- var clusterId = requirePresent(source.clusterId(), "clusterId");
- return Text.format("Cluster %s in %s.%s for %s.%s",
- clusterId.value(),
- zone.environment(), zone.region(),
- application, instance);
- }
-
-
- private static <T> T requirePresent(Optional<T> optional, String field) {
- return optional.orElseThrow(() -> new MissingOptionalException(field));
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationSource.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationSource.java
deleted file mode 100644
index 72d3dd933aa..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationSource.java
+++ /dev/null
@@ -1,152 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalLong;
-
-/**
- * Denotes the source of the notification.
- *
- * @author freva
- */
-public class NotificationSource {
- private final TenantName tenant;
- private final Optional<ApplicationName> application;
- private final Optional<InstanceName> instance;
- private final Optional<ZoneId> zoneId;
- private final Optional<ClusterSpec.Id> clusterId;
- private final Optional<JobType> jobType;
- private final OptionalLong runNumber;
-
- public NotificationSource(TenantName tenant, Optional<ApplicationName> application, Optional<InstanceName> instance,
- Optional<ZoneId> zoneId, Optional<ClusterSpec.Id> clusterId, Optional<JobType> jobType, OptionalLong runNumber) {
- this.tenant = Objects.requireNonNull(tenant, "tenant cannot be null");
- this.application = Objects.requireNonNull(application, "application cannot be null");
- this.instance = Objects.requireNonNull(instance, "instance cannot be null");
- this.zoneId = Objects.requireNonNull(zoneId, "zoneId cannot be null");
- this.clusterId = Objects.requireNonNull(clusterId, "clusterId cannot be null");
- this.jobType = Objects.requireNonNull(jobType, "jobType cannot be null");
- this.runNumber = Objects.requireNonNull(runNumber, "runNumber cannot be null");
-
- if (instance.isPresent() && application.isEmpty())
- throw new IllegalArgumentException("Application name must be present with instance name");
- if (zoneId.isPresent() && instance.isEmpty())
- throw new IllegalArgumentException("Instance name must be present with zone ID");
- if (clusterId.isPresent() && zoneId.isEmpty())
- throw new IllegalArgumentException("Zone ID must be present with cluster ID");
- if (clusterId.isPresent() && jobType.isPresent())
- throw new IllegalArgumentException("Cannot set both cluster ID and job type");
- if (jobType.isPresent() && instance.isEmpty())
- throw new IllegalArgumentException("Instance name must be present with job type");
- if (jobType.isPresent() != runNumber.isPresent())
- throw new IllegalArgumentException(Text.format("Run number (%s) must be 1-to-1 with job type (%s)",
- runNumber.isPresent() ? "present" : "missing", jobType.map(i -> "present").orElse("missing")));
- }
-
-
- public TenantName tenant() { return tenant; }
- public Optional<ApplicationName> application() { return application; }
- public Optional<InstanceName> instance() { return instance; }
- public Optional<ZoneId> zoneId() { return zoneId; }
- public Optional<ClusterSpec.Id> clusterId() { return clusterId; }
- public Optional<JobType> jobType() { return jobType; }
- public OptionalLong runNumber() { return runNumber; }
-
- /**
- * Returns true iff this source contains the given source. A source contains the other source if
- * all the set fields in this source are equal to the given source, while the fields not set
- * in this source are ignored.
- */
- public boolean contains(NotificationSource other) {
- return tenant.equals(other.tenant) &&
- (application.isEmpty() || application.equals(other.application)) &&
- (instance.isEmpty() || instance.equals(other.instance)) &&
- (zoneId.isEmpty() || zoneId.equals(other.zoneId)) &&
- (clusterId.isEmpty() || clusterId.equals(other.clusterId)) &&
- (jobType.isEmpty() || jobType.equals(other.jobType)); // Do not consider run number (it's unique!)
- }
-
- /**
- * Returns whether this source from a production deployment or deployment related to prod deployment (e.g. to
- * staging zone), or if this is at tenant or application level
- */
- public boolean isProduction() {
- return ! zoneId.map(ZoneId::environment)
- .or(() -> jobType.map(JobType::environment))
- .map(Environment::isManuallyDeployed)
- .orElse(false);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- NotificationSource that = (NotificationSource) o;
- return tenant.equals(that.tenant) && application.equals(that.application) && instance.equals(that.instance) &&
- zoneId.equals(that.zoneId) && clusterId.equals(that.clusterId) && jobType.equals(that.jobType); // Do not consider run number (it's unique!)
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(tenant, application, instance, zoneId, clusterId, jobType, runNumber);
- }
-
- @Override
- public String toString() {
- return "NotificationSource{" +
- "tenant=" + tenant +
- application.map(application -> ", application=" + application.value()).orElse("") +
- instance.map(instance -> ", instance=" + instance.value()).orElse("") +
- zoneId.map(zoneId -> ", zone=" + zoneId.value()).orElse("") +
- clusterId.map(clusterId -> ", clusterId=" + clusterId.value()).orElse("") +
- jobType.map(jobType -> ", job=" + jobType.jobName() + "#" + runNumber.getAsLong()).orElse("") +
- '}';
- }
-
- private static NotificationSource from(TenantName tenant, ApplicationName application, InstanceName instance, ZoneId zoneId,
- ClusterSpec.Id clusterId, JobType jobType, Long runNumber) {
- return new NotificationSource(tenant, Optional.ofNullable(application), Optional.ofNullable(instance), Optional.ofNullable(zoneId),
- Optional.ofNullable(clusterId), Optional.ofNullable(jobType), runNumber == null ? OptionalLong.empty() : OptionalLong.of(runNumber));
- }
-
- public static NotificationSource from(TenantName tenantName) {
- return from(tenantName, null, null, null, null, null, null);
- }
-
- public static NotificationSource from(TenantAndApplicationId id) {
- return from(id.tenant(), id.application(), null, null, null, null, null);
- }
-
- public static NotificationSource from(ApplicationId app) {
- return from(app.tenant(), app.application(), app.instance(), null, null, null, null);
- }
-
- public static NotificationSource from(DeploymentId deploymentId) {
- ApplicationId app = deploymentId.applicationId();
- return from(app.tenant(), app.application(), app.instance(), deploymentId.zoneId(), null, null, null);
- }
-
- public static NotificationSource from(DeploymentId deploymentId, ClusterSpec.Id clusterId) {
- ApplicationId app = deploymentId.applicationId();
- return from(app.tenant(), app.application(), app.instance(), deploymentId.zoneId(), clusterId, null, null);
- }
-
- public static NotificationSource from(RunId runId) {
- ApplicationId app = runId.application();
- return from(app.tenant(), app.application(), app.instance(), null, null, runId.job().type(), runId.number());
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
deleted file mode 100644
index e279e4feacd..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDb.java
+++ /dev/null
@@ -1,265 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-import com.yahoo.collections.Pair;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.text.Text;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.notification.Notification.MailContent;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-
-import java.time.Clock;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Optional;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing.Cluster;
-import static com.yahoo.vespa.hosted.controller.notification.Notification.Level;
-import static com.yahoo.vespa.hosted.controller.notification.Notification.Type;
-import static com.yahoo.vespa.hosted.controller.notification.Notifier.notificationLink;
-
-/**
- * Adds, updates and removes tenant notifications in ZK
- *
- * @author freva
- */
-public class NotificationsDb {
-
- private static final Logger log = Logger.getLogger(NotificationsDb.class.getName());
-
- private final Clock clock;
- private final CuratorDb curatorDb;
- private final Notifier notifier;
- private final ConsoleUrls consoleUrls;
-
- public NotificationsDb(Controller controller) {
- this(controller.clock(), controller.curator(), controller.notifier(), controller.serviceRegistry().consoleUrls());
- }
-
- NotificationsDb(Clock clock, CuratorDb curatorDb, Notifier notifier, ConsoleUrls consoleUrls) {
- this.clock = clock;
- this.curatorDb = curatorDb;
- this.notifier = notifier;
- this.consoleUrls = consoleUrls;
- }
-
- public List<TenantName> listTenantsWithNotifications() {
- return curatorDb.listTenantsWithNotifications();
- }
-
- public List<Notification> listNotifications(NotificationSource source, boolean productionOnly) {
- return curatorDb.readNotifications(source.tenant()).stream()
- .filter(notification -> source.contains(notification.source()) && (!productionOnly || notification.source().isProduction()))
- .toList();
- }
-
- public void setSubmissionNotification(TenantAndApplicationId tenantApp, String message) {
- NotificationSource source = NotificationSource.from(tenantApp);
- String title = "Application package for [%s](%s) has a warning".formatted(
- tenantApp.application().value(), notificationLink(consoleUrls, source));
- setNotification(source, Type.submission, Level.warning, title, List.of(message), Optional.empty());
- }
-
- public void setApplicationPackageNotification(NotificationSource source, List<String> messages) {
- String title = "Application package for [%s%s](%s) has %s".formatted(
- source.application().get().value(), source.instance().map(i -> "." + i.value()).orElse(""), notificationLink(consoleUrls, source),
- messages.size() == 1 ? "a warning" : "warnings");
- setNotification(source, Type.applicationPackage, Level.warning, title, messages, Optional.empty());
- }
-
- public void setTestPackageNotification(TenantAndApplicationId tenantApp, List<String> messages) {
- NotificationSource source = NotificationSource.from(tenantApp);
- String title = "There %s with tests for [%s](%s)".formatted(
- messages.size() == 1 ? "is a problem" : "are problems", tenantApp.application().value(),
- notificationLink(consoleUrls, source));
- setNotification(source, Type.testPackage, Level.warning, title, messages, Optional.empty());
- }
-
- public void setDeploymentNotification(RunId runId, String message) {
- String description, linkText;
- if (runId.type().isProduction()) {
- description = runId.type().isTest() ? "Test job " : "Deployment job ";
- linkText = "#" + runId.number() + " to " + runId.type().zone().region().value();
- } else if (runId.type().isTest()) {
- description = "";
- linkText = (runId.type().isStagingTest() ? "Staging" : "System") + " test #" + runId.number();
- } else if (runId.type().isDeployment()) {
- description = "Deployment job ";
- linkText = "#" + runId.number() + " to " + runId.type().zone().value();
- } else throw new IllegalStateException("Unexpected job type " + runId.type());
- NotificationSource source = NotificationSource.from(runId);
- String title = "%s[%s](%s) for application **%s.%s** has failed".formatted(
- description, linkText, notificationLink(consoleUrls, source), runId.application().application().value(), runId.application().instance().value());
- setNotification(source, Type.deployment, Level.error, title, List.of(message), Optional.empty());
- }
-
- /**
- * Add a notification with given source and type. If a notification with same source and type
- * already exists, it'll be replaced by this one instead.
- */
- public void setNotification(NotificationSource source, Type type, Level level, String title, List<String> messages,
- Optional<MailContent> mailContent) {
- Optional<Notification> changed = Optional.empty();
- try (Mutex lock = curatorDb.lockNotifications(source.tenant())) {
- var existingNotifications = curatorDb.readNotifications(source.tenant());
- List<Notification> notifications = existingNotifications.stream()
- .filter(notification -> !source.equals(notification.source()) || type != notification.type())
- .collect(Collectors.toCollection(ArrayList::new));
- var notification = new Notification(clock.instant(), type, level, source, title, messages, mailContent);
- if (!notificationExists(notification, existingNotifications, false)) {
- changed = Optional.of(notification);
- }
- notifications.add(notification);
- curatorDb.writeNotifications(source.tenant(), notifications);
- }
- changed.ifPresent(c -> {
- log.fine(() -> "New notification %s".formatted(c));
- notifier.dispatch(c);
- });
- }
-
- /** Remove the notification with the given source and type */
- public void removeNotification(NotificationSource source, Type type) {
- try (Mutex lock = curatorDb.lockNotifications(source.tenant())) {
- List<Notification> initial = curatorDb.readNotifications(source.tenant());
- List<Notification> filtered = initial.stream()
- .filter(notification -> !source.equals(notification.source()) || type != notification.type())
- .toList();
- if (initial.size() > filtered.size())
- curatorDb.writeNotifications(source.tenant(), filtered);
- }
- }
-
- /** Remove all notifications for this source or sources contained by this source */
- public void removeNotifications(NotificationSource source) {
- try (Mutex lock = curatorDb.lockNotifications(source.tenant())) {
- if (source.application().isEmpty()) { // Source is tenant
- curatorDb.deleteNotifications(source.tenant());
- return;
- }
-
- List<Notification> initial = curatorDb.readNotifications(source.tenant());
- List<Notification> filtered = initial.stream()
- .filter(notification -> !source.contains(notification.source()))
- .toList();
- if (initial.size() > filtered.size())
- curatorDb.writeNotifications(source.tenant(), filtered);
- }
- }
-
- /**
- * Updates notifications based on deployment metrics (e.g. feed blocked and reindexing progress) for the given
- * deployment based on current cluster metrics.
- * Will clear notifications of any cluster not reporting the metrics or whose metrics indicate feed is not blocked
- * or reindexing no longer in progress. Will set notification for clusters:
- * - that are (Level.error) or are nearly (Level.warning) feed blocked,
- * - that are (Level.info) currently reindexing at least 1 document type.
- */
- public void setDeploymentMetricsNotifications(DeploymentId deploymentId, List<ClusterMetrics> clusterMetrics, ApplicationReindexing applicationReindexing) {
- Instant now = clock.instant();
- List<Notification> changed = List.of();
- List<Notification> newNotifications = Stream.concat(
- clusterMetrics.stream().map(metric -> createFeedBlockNotification(consoleUrls, deploymentId, metric.getClusterId(), now, metric)),
- applicationReindexing.clusters().entrySet().stream().map(entry ->
- createReindexNotification(consoleUrls, deploymentId, entry.getKey(), now, entry.getValue())))
- .flatMap(Optional::stream)
- .toList();
-
- NotificationSource deploymentSource = NotificationSource.from(deploymentId);
- try (Mutex lock = curatorDb.lockNotifications(deploymentSource.tenant())) {
- List<Notification> initial = curatorDb.readNotifications(deploymentSource.tenant());
- List<Notification> updated = Stream.concat(
- initial.stream()
- .filter(notification ->
- // Filter out old feed block notifications and reindex for this deployment
- (notification.type() != Type.feedBlock && notification.type() != Type.reindex) ||
- !deploymentSource.contains(notification.source())),
- // ... and add the new notifications for this deployment
- newNotifications.stream())
- .toList();
- if (!initial.equals(updated)) {
- curatorDb.writeNotifications(deploymentSource.tenant(), updated);
- }
- changed = newNotifications.stream().filter(n -> !notificationExists(n, initial, true)).toList();
- }
- notifier.dispatch(changed, deploymentSource);
- }
-
- private boolean notificationExists(Notification notification, List<Notification> existing, boolean mindHigherLevel) {
- // Be conservative for now, only dispatch notifications if they are from new source or with new type.
- // the message content and level is ignored for now
- boolean exists = existing.stream()
- .anyMatch(e -> notification.source().contains(e.source()) && notification.type().equals(e.type()) &&
- (!mindHigherLevel || notification.level().ordinal() <= e.level().ordinal()));
- log.fine(() -> "%s in %s == %b".formatted(notification, existing, exists));
- return exists;
- }
-
- private static Optional<Notification> createFeedBlockNotification(ConsoleUrls consoleUrls, DeploymentId deployment, String clusterId, Instant at, ClusterMetrics metric) {
- Optional<Pair<Level, String>> memoryStatus =
- resourceUtilToFeedBlockStatus("memory", metric.memoryUtil(), metric.memoryFeedBlockLimit());
- Optional<Pair<Level, String>> diskStatus =
- resourceUtilToFeedBlockStatus("disk", metric.diskUtil(), metric.diskFeedBlockLimit());
- if (memoryStatus.isEmpty() && diskStatus.isEmpty()) return Optional.empty();
-
- NotificationSource source = NotificationSource.from(deployment, ClusterSpec.Id.from(clusterId));
- // Find the max among levels
- Level level = Stream.of(memoryStatus, diskStatus)
- .flatMap(status -> status.stream().map(Pair::getFirst))
- .max(Comparator.comparing(Enum::ordinal)).get();
- String title = "Cluster [%s](%s) in **%s** for **%s.%s** is %sfeed blocked".formatted(
- clusterId, notificationLink(consoleUrls, source), deployment.zoneId().value(), deployment.applicationId().application().value(),
- deployment.applicationId().instance().value(), level == Level.warning ? "nearly " : "");
- List<String> messages = Stream.concat(memoryStatus.stream(), diskStatus.stream())
- .filter(status -> status.getFirst() == level) // Do not mix message from different levels
- .map(Pair::getSecond)
- .toList();
-
- return Optional.of(new Notification(at, Type.feedBlock, level, source, title, messages));
- }
-
- private static Optional<Notification> createReindexNotification(ConsoleUrls consoleUrls, DeploymentId deployment, String clusterId, Instant at, Cluster cluster) {
- NotificationSource source = NotificationSource.from(deployment, ClusterSpec.Id.from(clusterId));
- String title = "Cluster [%s](%s) in **%s** for **%s.%s** is [reindexing](https://docs.vespa.ai/en/operations/reindexing.html)".formatted(
- clusterId, consoleUrls.clusterReindexing(deployment.applicationId(), deployment.zoneId(), source.clusterId().get()),
- deployment.zoneId().value(), deployment.applicationId().application().value(), deployment.applicationId().instance().value());
- List<String> messages = cluster.ready().entrySet().stream()
- .filter(entry -> entry.getValue().progress().isPresent())
- .map(entry -> Text.format("document type '%s'%s (%.1f%% done)",
- entry.getKey(), entry.getValue().cause().map(s -> " " + s).orElse(""), 100 * entry.getValue().progress().get()))
- .sorted()
- .toList();
- if (messages.isEmpty()) return Optional.empty();
- return Optional.of(new Notification(at, Type.reindex, Level.info, source, title, messages));
- }
-
- /**
- * Returns a feed block summary for the given resource: the notification level and
- * notification message for the given resource utilization wrt. given resource limit.
- * If utilization is well below the limit, Optional.empty() is returned.
- */
- private static Optional<Pair<Level, String>> resourceUtilToFeedBlockStatus(
- String resource, Optional<Double> util, Optional<Double> feedBlockLimit) {
- if (util.isEmpty() || feedBlockLimit.isEmpty()) return Optional.empty();
- double utilRelativeToLimit = util.get() / feedBlockLimit.get();
- if (utilRelativeToLimit < 0.95) return Optional.empty();
-
- String message = Text.format("%s (usage: %.1f%%, feed block limit: %.1f%%)",
- resource, 100 * util.get(), 100 * feedBlockLimit.get());
- if (utilRelativeToLimit < 1) return Optional.of(new Pair<>(Level.warning, message));
- return Optional.of(new Pair<>(Level.error, message));
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
deleted file mode 100644
index f27e69c4636..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/notification/Notifier.java
+++ /dev/null
@@ -1,172 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Mail;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Mailer;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.MailerException;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Objects;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.regex.Pattern;
-import java.util.stream.Collectors;
-
-/**
- * Notifier is responsible for dispatching user notifications to their chosen Contact points.
- *
- * @author enygaard
- */
-public class Notifier {
- private final CuratorDb curatorDb;
- private final Mailer mailer;
- private final FlagSource flagSource;
- private final ConsoleUrls consoleUrls;
- private final NotificationFormatter formatter;
- private final MailTemplating mailTemplating;
-
- private static final Logger log = Logger.getLogger(Notifier.class.getName());
-
- // Minimal url pattern matcher to detect hardcoded URLs in Notification messages
- private static final Pattern urlPattern = Pattern.compile("https://[\\w\\d./]+");
-
- public Notifier(CuratorDb curatorDb, ConsoleUrls consoleUrls, Mailer mailer, FlagSource flagSource) {
- this.curatorDb = Objects.requireNonNull(curatorDb);
- this.mailer = Objects.requireNonNull(mailer);
- this.flagSource = Objects.requireNonNull(flagSource);
- this.consoleUrls = Objects.requireNonNull(consoleUrls);
- this.formatter = new NotificationFormatter(consoleUrls);
- this.mailTemplating = new MailTemplating(consoleUrls);
- }
-
- public void dispatch(List<Notification> notifications, NotificationSource source) {
- if (!dispatchEnabled(source) || skipSource(source)) {
- return;
- }
- if (notifications.isEmpty()) {
- return;
- }
- var tenant = curatorDb.readTenant(source.tenant());
- tenant.stream().forEach(t -> {
- if (t instanceof CloudTenant ct) {
- ct.info().contacts().all().stream()
- .filter(c -> c.audiences().contains(TenantContacts.Audience.NOTIFICATIONS))
- .collect(Collectors.groupingBy(TenantContacts.Contact::type, Collectors.toList()))
- .forEach((type, contacts) -> notifications.forEach(n -> dispatch(n, type, contacts)));
- }
- });
- }
-
- public void dispatch(Notification notification) {
- dispatch(List.of(notification), notification.source());
- }
-
- private boolean dispatchEnabled(NotificationSource source) {
- return PermanentFlags.NOTIFICATION_DISPATCH_FLAG.bindTo(flagSource)
- .with(FetchVector.Dimension.TENANT_ID, source.tenant().value())
- .value();
- }
-
- private boolean skipSource(NotificationSource source) {
- // Do not dispatch notification for dev and perf environments
- return source.zoneId()
- .map(z -> z.environment())
- .map(e -> e == Environment.dev || e == Environment.perf)
- .orElse(false);
- }
-
- private void dispatch(Notification notification, TenantContacts.Type type, Collection<? extends TenantContacts.Contact> contacts) {
- switch (type) {
- case EMAIL -> dispatch(notification, contacts.stream().map(c -> (TenantContacts.EmailContact) c).toList());
- default -> throw new IllegalArgumentException("Unknown TenantContacts type " + type.name());
- }
- }
-
- private void dispatch(Notification notification, Collection<TenantContacts.EmailContact> contacts) {
- try {
- log.fine(() -> "Sending notification " + notification + " to " +
- contacts.stream().map(c -> c.email().getEmailAddress()).toList());
- var content = formatter.format(notification);
- var verifiedContacts = contacts.stream()
- .filter(c -> c.email().isVerified()).map(c -> c.email().getEmailAddress()).toList();
- if (verifiedContacts.isEmpty()) {
- log.fine(() -> "None of the %d contact(s) are verified - skipping delivery of %s".formatted(contacts.size(), notification));
- return;
- }
- mailer.send(mailOf(content, verifiedContacts));
- } catch (MailerException e) {
- log.log(Level.SEVERE, "Failed sending email", e);
- } catch (MissingOptionalException e) {
- log.log(Level.WARNING, "Missing value in required field '" + e.field() + "' for notification type: " + notification.type(), e);
- }
- }
-
- public Mail mailOf(FormattedNotification content, Collection<String> recipients) {
- var notification = content.notification();
- var subject = content.notification().mailContent().flatMap(Notification.MailContent::subject)
- .orElseGet(() -> Text.format(
- "[%s] %s Vespa Notification for %s", notification.level().toString().toUpperCase(),
- content.prettyType(), applicationIdSource(notification.source())));
- var html = generateHtml(content);
- return new Mail(recipients, subject, "", html);
- }
-
- private String generateHtml(FormattedNotification content) {
- var mailContent = content.notification().mailContent().orElseGet(() -> generateContentFromMessages(content));
- return mailTemplating.generateDefaultMailHtml(mailContent.template(), mailContent.values(), content.notification().source().tenant());
- }
-
- private Notification.MailContent generateContentFromMessages(FormattedNotification f) {
- var items = f.notification().messages().stream().map(m -> capitalise(linkify(mailTemplating.escapeHtml(m)))).toList();
- return Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT)
- .with("mailMessageTemplate", "notification-message")
- .with("mailTitle", "Vespa Cloud Notifications")
- .with("notificationHeader", f.messagePrefix())
- .with("notificationItems", items)
- .with("consoleLink", notificationLink(consoleUrls, f.notification().source()))
- .build();
- }
-
- @VisibleForTesting
- static String linkify(String text) {
- return urlPattern.matcher(text).replaceAll((res) -> String.format("<a href=\"%s\">%s</a>", res.group(), res.group()));
- }
-
- private String applicationIdSource(NotificationSource source) {
- StringBuilder sb = new StringBuilder();
- sb.append(source.tenant().value());
- source.application().ifPresent(applicationName -> sb.append(".").append(applicationName.value()));
- source.instance().ifPresent(instanceName -> sb.append(".").append(instanceName.value()));
- return sb.toString();
- }
-
- static String notificationLink(ConsoleUrls consoleUrls, NotificationSource source) {
- if (source.application().isEmpty()) return consoleUrls.tenantOverview(source.tenant());
- if (source.instance().isEmpty()) return consoleUrls.prodApplicationOverview(source.tenant(), source.application().get());
-
- ApplicationId application = ApplicationId.from(source.tenant(), source.application().get(), source.instance().get());
- if (source.jobType().isPresent())
- return consoleUrls.deploymentRun(new RunId(application, source.jobType().get(), source.runNumber().getAsLong()));
- if (source.clusterId().isPresent())
- return consoleUrls.clusterOverview(application, source.zoneId().get(), source.clusterId().get());
- return consoleUrls.instanceOverview(application, source.zoneId().map(ZoneId::environment).orElse(Environment.prod));
- }
-
- private static String capitalise(String m) {
- return m.substring(0, 1).toUpperCase() + m.substring(1);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/package-info.java
deleted file mode 100644
index 22d10386d7f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/package-info.java
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * The root package of the controller
- *
- * @author bratseth
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
deleted file mode 100644
index 07fac67100f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializer.java
+++ /dev/null
@@ -1,575 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.ObjectTraverser;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.AccountId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentActivity;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
-import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationId;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationState;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationStatus;
-
-import java.security.PublicKey;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.OptionalLong;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static java.util.stream.Collectors.toMap;
-
-/**
- * Serializes {@link Application}s to/from slime.
- * This class is multithread safe.
- *
- * @author jonmv
- * @author mpolden
- */
-public class ApplicationSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- // Application fields
- private static final String idField = "id";
- private static final String createdAtField = "createdAt";
- private static final String deploymentSpecField = "deploymentSpecField";
- private static final String validationOverridesField = "validationOverrides";
- private static final String instancesField = "instances";
- private static final String deployingField = "deployingField";
- private static final String projectIdField = "projectId";
- private static final String versionsField = "versions";
- private static final String prodVersionsField = "prodVersions";
- private static final String devVersionsField = "devVersions";
- private static final String platformPinnedField = "pinned";
- private static final String revisionPinnedField = "revisionPinned";
- private static final String deploymentIssueField = "deploymentIssueId";
- private static final String ownershipIssueIdField = "ownershipIssueId";
- private static final String userOwnerField = "confirmedOwner";
- private static final String issueOwnerField = "confirmedOwnerId";
- private static final String majorVersionField = "majorVersion";
- private static final String writeQualityField = "writeQuality";
- private static final String queryQualityField = "queryQuality";
- private static final String pemDeployKeysField = "pemDeployKeys";
- private static final String assignedRotationClusterField = "clusterId";
- private static final String assignedRotationRotationField = "rotationId";
- private static final String assignedRotationRegionsField = "regions";
- private static final String versionField = "version";
-
- // Instance fields
- private static final String instanceNameField = "instanceName";
- private static final String deploymentsField = "deployments";
- private static final String deploymentJobsField = "deploymentJobs"; // TODO jonmv: clean up serialisation format
- private static final String assignedRotationsField = "assignedRotations";
- private static final String assignedRotationEndpointField = "endpointId";
-
- // Deployment fields
- private static final String zoneField = "zone";
- private static final String cloudAccountField = "cloudAccount";
- private static final String environmentField = "environment";
- private static final String regionField = "region";
- private static final String deployTimeField = "deployTime";
- private static final String applicationBuildNumberField = "applicationBuildNumber";
- private static final String applicationPackageRevisionField = "applicationPackageRevision";
- private static final String sourceRevisionField = "sourceRevision";
- private static final String repositoryField = "repositoryField";
- private static final String branchField = "branchField";
- private static final String commitField = "commitField";
- private static final String descriptionField = "description";
- private static final String submittedAtField = "submittedAt";
- private static final String riskField = "risk";
- private static final String authorEmailField = "authorEmailField";
- private static final String deployedDirectlyField = "deployedDirectly";
- private static final String obsoleteAtField = "obsoleteAt";
- private static final String hasPackageField = "hasPackage";
- private static final String shouldSkipField = "shouldSkip";
- private static final String compileVersionField = "compileVersion";
- private static final String allowedMajorField = "allowedMajor";
- private static final String buildTimeField = "buildTime";
- private static final String sourceUrlField = "sourceUrl";
- private static final String bundleHashField = "bundleHash";
- private static final String lastQueriedField = "lastQueried";
- private static final String lastWrittenField = "lastWritten";
- private static final String lastQueriesPerSecondField = "lastQueriesPerSecond";
- private static final String lastWritesPerSecondField = "lastWritesPerSecond";
- private static final String dataPlaneTokensField = "dataPlaneTokens";
- private static final String tokenIdField = "id";
- private static final String tokenUpdatedField = "updated";
-
- // DeploymentJobs fields
- private static final String jobStatusField = "jobStatus";
-
- // JobStatus field
- private static final String jobTypeField = "jobType";
- private static final String pausedUntilField = "pausedUntil";
-
- // Deployment metrics fields
- private static final String deploymentMetricsField = "metrics";
- private static final String deploymentMetricsQPSField = "queriesPerSecond";
- private static final String deploymentMetricsWPSField = "writesPerSecond";
- private static final String deploymentMetricsDocsField = "documentCount";
- private static final String deploymentMetricsQueryLatencyField = "queryLatencyMillis";
- private static final String deploymentMetricsWriteLatencyField = "writeLatencyMillis";
- private static final String deploymentMetricsUpdateTime = "lastUpdated";
- private static final String deploymentMetricsWarningsField = "warnings";
-
- // RotationStatus fields
- private static final String rotationStatusField = "rotationStatus2";
- private static final String rotationIdField = "rotationId";
- private static final String lastUpdatedField = "lastUpdated";
- private static final String rotationStateField = "state";
- private static final String statusField = "status";
-
- // Quota usage fields
- private static final String quotaUsageRateField = "quotaUsageRate";
-
- private static final String deploymentCostField = "cost";
-
- // ------------------ Serialization
-
- public Slime toSlime(Application application) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- root.setString(idField, application.id().serialized());
- root.setLong(createdAtField, application.createdAt().toEpochMilli());
- root.setString(deploymentSpecField, application.deploymentSpec().xmlForm());
- root.setString(validationOverridesField, application.validationOverrides().xmlForm());
- application.projectId().ifPresent(projectId -> root.setLong(projectIdField, projectId));
- application.deploymentIssueId().ifPresent(jiraIssueId -> root.setString(deploymentIssueField, jiraIssueId.value()));
- application.ownershipIssueId().ifPresent(issueId -> root.setString(ownershipIssueIdField, issueId.value()));
- application.userOwner().ifPresent(owner -> root.setString(userOwnerField, owner.username()));
- application.issueOwner().ifPresent(owner -> root.setString(issueOwnerField, owner.value()));
- application.majorVersion().ifPresent(majorVersion -> root.setLong(majorVersionField, majorVersion));
- root.setDouble(queryQualityField, application.metrics().queryServiceQuality());
- root.setDouble(writeQualityField, application.metrics().writeServiceQuality());
- deployKeysToSlime(application.deployKeys(), root.setArray(pemDeployKeysField));
- revisionsToSlime(application.revisions(), root.setArray(prodVersionsField), root.setArray(devVersionsField));
- instancesToSlime(application, root.setArray(instancesField));
- return slime;
- }
-
- private void instancesToSlime(Application application, Cursor array) {
- for (Instance instance : application.instances().values()) {
- Cursor instanceObject = array.addObject();
- instanceObject.setString(instanceNameField, instance.name().value());
- deploymentsToSlime(instance.deployments().values(), instanceObject.setArray(deploymentsField));
- toSlime(instance.jobPauses(), instanceObject.setObject(deploymentJobsField));
- assignedRotationsToSlime(instance.rotations(), instanceObject);
- toSlime(instance.rotationStatus(), instanceObject.setArray(rotationStatusField));
- toSlime(instance.change(), instanceObject, deployingField);
- }
- }
-
- private void deployKeysToSlime(Set<PublicKey> deployKeys, Cursor array) {
- deployKeys.forEach(key -> array.addString(KeyUtils.toPem(key)));
- }
-
- private void deploymentsToSlime(Collection<Deployment> deployments, Cursor array) {
- for (Deployment deployment : deployments)
- deploymentToSlime(deployment, array.addObject());
- }
-
- private void deploymentToSlime(Deployment deployment, Cursor object) {
- zoneIdToSlime(deployment.zone(), object.setObject(zoneField));
- if (!deployment.cloudAccount().isUnspecified()) object.setString(cloudAccountField, deployment.cloudAccount().value());
- object.setString(versionField, deployment.version().toString());
- object.setLong(deployTimeField, deployment.at().toEpochMilli());
- toSlime(deployment.revision(), object.setObject(applicationPackageRevisionField));
- deploymentMetricsToSlime(deployment.metrics(), object);
- deployment.activity().lastQueried().ifPresent(instant -> object.setLong(lastQueriedField, instant.toEpochMilli()));
- deployment.activity().lastWritten().ifPresent(instant -> object.setLong(lastWrittenField, instant.toEpochMilli()));
- deployment.activity().lastQueriesPerSecond().ifPresent(value -> object.setDouble(lastQueriesPerSecondField, value));
- deployment.activity().lastWritesPerSecond().ifPresent(value -> object.setDouble(lastWritesPerSecondField, value));
- object.setDouble(quotaUsageRateField, deployment.quota().rate());
- deployment.cost().ifPresent(cost -> object.setDouble(deploymentCostField, cost));
- Cursor dataPlaneTokensArray = object.setArray(dataPlaneTokensField);
- deployment.dataPlaneTokens().forEach((id, updated) -> {
- Cursor tokenObject = dataPlaneTokensArray.addObject();
- tokenObject.setString(tokenIdField, id.value());
- tokenObject.setLong(tokenUpdatedField, updated.toEpochMilli());
- });
- }
-
- private void deploymentMetricsToSlime(DeploymentMetrics metrics, Cursor object) {
- Cursor root = object.setObject(deploymentMetricsField);
- root.setDouble(deploymentMetricsQPSField, metrics.queriesPerSecond());
- root.setDouble(deploymentMetricsWPSField, metrics.writesPerSecond());
- root.setDouble(deploymentMetricsDocsField, metrics.documentCount());
- root.setDouble(deploymentMetricsQueryLatencyField, metrics.queryLatencyMillis());
- root.setDouble(deploymentMetricsWriteLatencyField, metrics.writeLatencyMillis());
- metrics.instant().ifPresent(instant -> root.setLong(deploymentMetricsUpdateTime, instant.toEpochMilli()));
- if (!metrics.warnings().isEmpty()) {
- Cursor warningsObject = root.setObject(deploymentMetricsWarningsField);
- metrics.warnings().forEach((warning, count) -> warningsObject.setLong(warning.name(), count));
- }
- }
-
- private void zoneIdToSlime(ZoneId zone, Cursor object) {
- object.setString(environmentField, zone.environment().value());
- object.setString(regionField, zone.region().value());
- }
-
- private void revisionsToSlime(RevisionHistory revisions, Cursor revisionsArray, Cursor devRevisionsArray) {
- revisionsToSlime(revisions.production(), revisionsArray);
- revisions.development().forEach((job, devRevisions) -> {
- Cursor devRevisionsObject = devRevisionsArray.addObject();
- devRevisionsObject.setString(instanceNameField, job.application().instance().value());
- devRevisionsObject.setString(jobTypeField, job.type().serialized());
- revisionsToSlime(devRevisions, devRevisionsObject.setArray(versionsField));
- });
- }
-
- private void revisionsToSlime(Iterable<ApplicationVersion> revisions, Cursor revisionsArray) {
- revisions.forEach(version -> toSlime(version, revisionsArray.addObject()));
- }
-
- private void toSlime(RevisionId revision, Cursor object) {
- object.setLong(applicationBuildNumberField, revision.number());
- object.setBool(deployedDirectlyField, ! revision.isProduction());
- }
-
- private void toSlime(ApplicationVersion applicationVersion, Cursor object) {
- object.setLong(applicationBuildNumberField, applicationVersion.buildNumber());
- applicationVersion.source().ifPresent(source -> toSlime(source, object.setObject(sourceRevisionField)));
- applicationVersion.authorEmail().ifPresent(email -> object.setString(authorEmailField, email));
- applicationVersion.compileVersion().ifPresent(version -> object.setString(compileVersionField, version.toString()));
- applicationVersion.allowedMajor().ifPresent(major -> object.setLong(allowedMajorField, major));
- applicationVersion.buildTime().ifPresent(time -> object.setLong(buildTimeField, time.toEpochMilli()));
- applicationVersion.sourceUrl().ifPresent(url -> object.setString(sourceUrlField, url));
- applicationVersion.commit().ifPresent(commit -> object.setString(commitField, commit));
- object.setBool(deployedDirectlyField, applicationVersion.isDeployedDirectly());
- applicationVersion.obsoleteAt().ifPresent(at -> object.setLong(obsoleteAtField, at.toEpochMilli()));
- object.setBool(hasPackageField, applicationVersion.hasPackage());
- object.setBool(shouldSkipField, applicationVersion.shouldSkip());
- applicationVersion.description().ifPresent(description -> object.setString(descriptionField, description));
- applicationVersion.submittedAt().ifPresent(at -> object.setLong(submittedAtField, at.toEpochMilli()));
- if (applicationVersion.risk() != 0) object.setLong(riskField, applicationVersion.risk());
- applicationVersion.bundleHash().ifPresent(bundleHash -> object.setString(bundleHashField, bundleHash));
- }
-
- private void toSlime(SourceRevision sourceRevision, Cursor object) {
- object.setString(repositoryField, sourceRevision.repository());
- object.setString(branchField, sourceRevision.branch());
- object.setString(commitField, sourceRevision.commit());
- }
-
- private void toSlime(Map<JobType, Instant> jobPauses, Cursor cursor) {
- Cursor jobStatusArray = cursor.setArray(jobStatusField);
- jobPauses.forEach((type, until) -> {
- Cursor jobPauseObject = jobStatusArray.addObject();
- jobPauseObject.setString(jobTypeField, type.serialized());
- jobPauseObject.setLong(pausedUntilField, until.toEpochMilli());
- });
- }
-
- private void toSlime(Change deploying, Cursor parentObject, String fieldName) {
- if (deploying.isEmpty()) return;
-
- Cursor object = parentObject.setObject(fieldName);
- if (deploying.platform().isPresent())
- object.setString(versionField, deploying.platform().get().toString());
- if (deploying.revision().isPresent())
- toSlime(deploying.revision().get(), object);
- if (deploying.isPlatformPinned())
- object.setBool(platformPinnedField, true);
- if (deploying.isRevisionPinned())
- object.setBool(revisionPinnedField, true);
- }
-
- private void toSlime(RotationStatus status, Cursor array) {
- status.asMap().forEach((rotationId, targets) -> {
- Cursor rotationObject = array.addObject();
- rotationObject.setString(rotationIdField, rotationId.asString());
- rotationObject.setLong(lastUpdatedField, targets.lastUpdated().toEpochMilli());
- Cursor statusArray = rotationObject.setArray(statusField);
- targets.asMap().forEach((zone, state) -> {
- Cursor statusObject = statusArray.addObject();
- zoneIdToSlime(zone, statusObject);
- statusObject.setString(rotationStateField, state.name());
- });
- });
- }
-
- private void assignedRotationsToSlime(List<AssignedRotation> rotations, Cursor parent) {
- var rotationsArray = parent.setArray(assignedRotationsField);
- for (var rotation : rotations) {
- var object = rotationsArray.addObject();
- object.setString(assignedRotationEndpointField, rotation.endpointId().id());
- object.setString(assignedRotationRotationField, rotation.rotationId().asString());
- object.setString(assignedRotationClusterField, rotation.clusterId().value());
- var regionsArray = object.setArray(assignedRotationRegionsField);
- for (var region : rotation.regions()) {
- regionsArray.addString(region.value());
- }
- }
- }
-
- // ------------------ Deserialization
-
- public Application fromSlime(byte[] data) {
- return fromSlime(SlimeUtils.jsonToSlime(data));
- }
-
- private Application fromSlime(Slime slime) {
- Inspector root = slime.get();
-
- TenantAndApplicationId id = TenantAndApplicationId.fromSerialized(root.field(idField).asString());
- Instant createdAt = SlimeUtils.instant(root.field(createdAtField));
- DeploymentSpec deploymentSpec = DeploymentSpec.fromXml(root.field(deploymentSpecField).asString(), false);
- ValidationOverrides validationOverrides = ValidationOverrides.fromXml(root.field(validationOverridesField).asString());
- Optional<IssueId> deploymentIssueId = SlimeUtils.optionalString(root.field(deploymentIssueField)).map(IssueId::from);
- Optional<IssueId> ownershipIssueId = SlimeUtils.optionalString(root.field(ownershipIssueIdField)).map(IssueId::from);
- Optional<User> userOwner = SlimeUtils.optionalString(root.field(userOwnerField)).map(User::from);
- Optional<AccountId> issueOwner = SlimeUtils.optionalString(root.field(issueOwnerField)).map(AccountId::new);
- OptionalInt majorVersion = SlimeUtils.optionalInteger(root.field(majorVersionField));
- ApplicationMetrics metrics = new ApplicationMetrics(root.field(queryQualityField).asDouble(),
- root.field(writeQualityField).asDouble());
- Set<PublicKey> deployKeys = deployKeysFromSlime(root.field(pemDeployKeysField));
- List<Instance> instances = instancesFromSlime(id, root.field(instancesField));
- OptionalLong projectId = SlimeUtils.optionalLong(root.field(projectIdField));
- RevisionHistory revisions = revisionsFromSlime(root.field(prodVersionsField), root.field(devVersionsField), id);
-
- return new Application(id, createdAt, deploymentSpec, validationOverrides,
- deploymentIssueId, ownershipIssueId, userOwner, issueOwner, majorVersion, metrics,
- deployKeys, projectId, revisions, instances);
- }
-
- private RevisionHistory revisionsFromSlime(Inspector prodVersionsArray, Inspector devVersionsArray, TenantAndApplicationId id) {
- List<ApplicationVersion> revisions = revisionsFromSlime(prodVersionsArray, null);
- Map<JobId, List<ApplicationVersion>> devRevisions = new HashMap<>();
- devVersionsArray.traverse((ArrayTraverser) (__, devRevisionsObject) -> {
- JobId job = jobIdFromSlime(id, devRevisionsObject);
- devRevisions.put(job, revisionsFromSlime(devRevisionsObject.field(versionsField), job));
- });
-
- return RevisionHistory.ofRevisions(revisions, devRevisions);
- }
-
- private JobId jobIdFromSlime(TenantAndApplicationId base, Inspector idObject) {
- return new JobId(base.instance(idObject.field(instanceNameField).asString()),
- JobType.ofSerialized(idObject.field(jobTypeField).asString()));
- }
-
- private List<ApplicationVersion> revisionsFromSlime(Inspector versionsArray, JobId job) {
- List<ApplicationVersion> revisions = new ArrayList<>();
- versionsArray.traverse((ArrayTraverser) (__, revisionObject) -> revisions.add(applicationVersionFromSlime(revisionObject, job)));
- return revisions;
- }
-
- private List<Instance> instancesFromSlime(TenantAndApplicationId id, Inspector field) {
- List<Instance> instances = new ArrayList<>();
- field.traverse((ArrayTraverser) (name, object) -> {
- InstanceName instanceName = InstanceName.from(object.field(instanceNameField).asString());
- List < Deployment > deployments = deploymentsFromSlime(object.field(deploymentsField), id.instance(instanceName));
- Map<JobType, Instant> jobPauses = jobPausesFromSlime(object.field(deploymentJobsField));
- List<AssignedRotation> assignedRotations = assignedRotationsFromSlime(object);
- RotationStatus rotationStatus = rotationStatusFromSlime(object);
- Change change = changeFromSlime(object.field(deployingField));
- instances.add(new Instance(id.instance(instanceName),
- deployments,
- jobPauses,
- assignedRotations,
- rotationStatus,
- change));
- });
- return instances;
- }
-
- private Set<PublicKey> deployKeysFromSlime(Inspector array) {
- Set<PublicKey> keys = new LinkedHashSet<>();
- array.traverse((ArrayTraverser) (__, key) -> keys.add(KeyUtils.fromPemEncodedPublicKey(key.asString())));
- return keys;
- }
-
- private List<Deployment> deploymentsFromSlime(Inspector array, ApplicationId id) {
- List<Deployment> deployments = new ArrayList<>();
- array.traverse((ArrayTraverser) (int i, Inspector item) -> deployments.add(deploymentFromSlime(item, id)));
- return deployments;
- }
-
- private Deployment deploymentFromSlime(Inspector deploymentObject, ApplicationId id) {
- ZoneId zone = zoneIdFromSlime(deploymentObject.field(zoneField));
- return new Deployment(zone,
- SlimeUtils.optionalString(deploymentObject.field(cloudAccountField)).map(CloudAccount::from).orElse(CloudAccount.empty),
- revisionFromSlime(deploymentObject.field(applicationPackageRevisionField), new JobId(id, JobType.deploymentTo(zone))),
- Version.fromString(deploymentObject.field(versionField).asString()),
- SlimeUtils.instant(deploymentObject.field(deployTimeField)),
- deploymentMetricsFromSlime(deploymentObject.field(deploymentMetricsField)),
- DeploymentActivity.create(SlimeUtils.optionalInstant(deploymentObject.field(lastQueriedField)),
- SlimeUtils.optionalInstant(deploymentObject.field(lastWrittenField)),
- SlimeUtils.optionalDouble(deploymentObject.field(lastQueriesPerSecondField)),
- SlimeUtils.optionalDouble(deploymentObject.field(lastWritesPerSecondField))),
- QuotaUsage.create(SlimeUtils.optionalDouble(deploymentObject.field(quotaUsageRateField))),
- SlimeUtils.optionalDouble(deploymentObject.field(deploymentCostField)),
- SlimeUtils.entriesStream(deploymentObject.field(dataPlaneTokensField))
- .collect(toMap(entry -> TokenId.of(entry.field(tokenIdField).asString()),
- entry -> Instant.ofEpochMilli(entry.field(tokenUpdatedField).asLong()))));
- }
-
- private DeploymentMetrics deploymentMetricsFromSlime(Inspector object) {
- Optional<Instant> instant = SlimeUtils.optionalInstant(object.field(deploymentMetricsUpdateTime));
- return new DeploymentMetrics(object.field(deploymentMetricsQPSField).asDouble(),
- object.field(deploymentMetricsWPSField).asDouble(),
- object.field(deploymentMetricsDocsField).asDouble(),
- object.field(deploymentMetricsQueryLatencyField).asDouble(),
- object.field(deploymentMetricsWriteLatencyField).asDouble(),
- instant,
- deploymentWarningsFrom(object.field(deploymentMetricsWarningsField)));
- }
-
- private Map<DeploymentMetrics.Warning, Integer> deploymentWarningsFrom(Inspector object) {
- Map<DeploymentMetrics.Warning, Integer> warnings = new HashMap<>();
- object.traverse((ObjectTraverser) (name, value) -> warnings.put(DeploymentMetrics.Warning.valueOf(name),
- (int) value.asLong()));
- return Collections.unmodifiableMap(warnings);
- }
-
- private RotationStatus rotationStatusFromSlime(Inspector parentObject) {
- var object = parentObject.field(rotationStatusField);
- var statusMap = new LinkedHashMap<RotationId, RotationStatus.Targets>();
- object.traverse((ArrayTraverser) (idx, statusObject) -> statusMap.put(new RotationId(statusObject.field(rotationIdField).asString()),
- new RotationStatus.Targets(
- singleRotationStatusFromSlime(statusObject.field(statusField)),
- SlimeUtils.instant(statusObject.field(lastUpdatedField)))));
- return RotationStatus.from(statusMap);
- }
-
- private Map<ZoneId, RotationState> singleRotationStatusFromSlime(Inspector object) {
- if (!object.valid()) {
- return Collections.emptyMap();
- }
- Map<ZoneId, RotationState> rotationStatus = new LinkedHashMap<>();
- object.traverse((ArrayTraverser) (idx, statusObject) -> {
- var zone = zoneIdFromSlime(statusObject);
- var status = RotationState.valueOf(statusObject.field(rotationStateField).asString());
- rotationStatus.put(zone, status);
- });
- return Collections.unmodifiableMap(rotationStatus);
- }
-
- private ZoneId zoneIdFromSlime(Inspector object) {
- return ZoneId.from(object.field(environmentField).asString(), object.field(regionField).asString());
- }
-
- private RevisionId revisionFromSlime(Inspector object, JobId job) {
- long build = object.field(applicationBuildNumberField).asLong();
- boolean production = object.field(deployedDirectlyField).valid() // TODO jonmv: remove after migration
- && build > 0
- && ! object.field(deployedDirectlyField).asBool();
- return production ? RevisionId.forProduction(build) : RevisionId.forDevelopment(build, job);
- }
-
- private ApplicationVersion applicationVersionFromSlime(Inspector object, JobId job) {
- RevisionId id = revisionFromSlime(object, job);
- Optional<SourceRevision> sourceRevision = sourceRevisionFromSlime(object.field(sourceRevisionField));
- Optional<String> authorEmail = SlimeUtils.optionalString(object.field(authorEmailField));
- Optional<Version> compileVersion = SlimeUtils.optionalString(object.field(compileVersionField)).map(Version::fromString);
- Optional<Integer> allowedMajor = SlimeUtils.optionalInteger(object.field(allowedMajorField)).stream().boxed().findFirst();
- Optional<Instant> buildTime = SlimeUtils.optionalInstant(object.field(buildTimeField));
- Optional<String> sourceUrl = SlimeUtils.optionalString(object.field(sourceUrlField));
- Optional<String> commit = SlimeUtils.optionalString(object.field(commitField));
- Optional<Instant> obsoleteAt = SlimeUtils.optionalInstant(object.field(obsoleteAtField));
- boolean hasPackage = object.field(hasPackageField).asBool();
- boolean shouldSkip = object.field(shouldSkipField).asBool();
- Optional<String> description = SlimeUtils.optionalString(object.field(descriptionField));
- Optional<Instant> submittedAt = SlimeUtils.optionalInstant(object.field(submittedAtField));
- int risk = (int) object.field(riskField).asLong();
- Optional<String> bundleHash = SlimeUtils.optionalString(object.field(bundleHashField));
-
- return new ApplicationVersion(id, sourceRevision, authorEmail, compileVersion, allowedMajor, buildTime,
- sourceUrl, commit, bundleHash, obsoleteAt, hasPackage, shouldSkip, description, submittedAt, risk);
- }
-
- private Optional<SourceRevision> sourceRevisionFromSlime(Inspector object) {
- if ( ! object.valid()) return Optional.empty();
- return Optional.of(new SourceRevision(object.field(repositoryField).asString(),
- object.field(branchField).asString(),
- object.field(commitField).asString()));
- }
-
- private Map<JobType, Instant> jobPausesFromSlime(Inspector object) {
- Map<JobType, Instant> jobPauses = new HashMap<>();
- object.field(jobStatusField).traverse((ArrayTraverser) (__, jobPauseObject) ->
- jobPauses.put(JobType.ofSerialized(jobPauseObject.field(jobTypeField).asString()),
- SlimeUtils.instant(jobPauseObject.field(pausedUntilField))));
- return jobPauses;
- }
-
- private Change changeFromSlime(Inspector object) {
- if ( ! object.valid()) return Change.empty();
- Inspector versionFieldValue = object.field(versionField);
- Change change = Change.empty();
- if (versionFieldValue.valid())
- change = Change.of(Version.fromString(versionFieldValue.asString()));
- if (object.field(applicationBuildNumberField).valid())
- change = change.with(revisionFromSlime(object, null));
- if (object.field(platformPinnedField).asBool())
- change = change.withPlatformPin();
- if (object.field(revisionPinnedField).asBool())
- change = change.withRevisionPin();
- return change;
- }
-
- private List<AssignedRotation> assignedRotationsFromSlime(Inspector root) {
- var assignedRotations = new LinkedHashMap<EndpointId, AssignedRotation>();
- root.field(assignedRotationsField).traverse((ArrayTraverser) (i, inspector) -> {
- var clusterId = new ClusterSpec.Id(inspector.field(assignedRotationClusterField).asString());
- var endpointId = EndpointId.of(inspector.field(assignedRotationEndpointField).asString());
- var rotationId = new RotationId(inspector.field(assignedRotationRotationField).asString());
- var regions = new LinkedHashSet<RegionName>();
- inspector.field(assignedRotationRegionsField).traverse((ArrayTraverser) (j, regionInspector) -> {
- regions.add(RegionName.from(regionInspector.asString()));
- });
- assignedRotations.putIfAbsent(endpointId, new AssignedRotation(clusterId, endpointId, rotationId, regions));
- });
-
- return List.copyOf(assignedRotations.values());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializer.java
deleted file mode 100644
index 40a3e35cb25..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializer.java
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.TenantManagedArchiveBucket;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.VespaManagedArchiveBucket;
-
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * (de)serializes tenant/bucket mappings for a zone
- *
- * @author andreer
- */
-public class ArchiveBucketsSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private final static String vespaManagedBucketsFieldName = "buckets";
- private final static String tenantManagedBucketsFieldName = "tenantManagedBuckets";
- private final static String bucketNameFieldName = "bucketName";
- private final static String keyArnFieldName = "keyArn";
- private final static String tenantsFieldName = "tenantIds";
- private final static String accountFieldName = "account";
- private final static String updatedAtFieldName = "updatedAt";
-
- public static Slime toSlime(ArchiveBuckets archiveBuckets) {
- Slime slime = new Slime();
- Cursor rootObject = slime.setObject();
-
- Cursor vespaBucketsArray = rootObject.setArray(vespaManagedBucketsFieldName);
- archiveBuckets.vespaManaged().forEach(bucket -> {
- Cursor cursor = vespaBucketsArray.addObject();
- cursor.setString(bucketNameFieldName, bucket.bucketName());
- cursor.setString(keyArnFieldName, bucket.keyArn());
- Cursor tenants = cursor.setArray(tenantsFieldName);
- bucket.tenants().forEach(tenantName -> tenants.addString(tenantName.value()));
- });
-
- Cursor tenantBucketsArray = rootObject.setArray(tenantManagedBucketsFieldName);
- archiveBuckets.tenantManaged().forEach(bucket -> {
- Cursor cursor = tenantBucketsArray.addObject();
- cursor.setString(bucketNameFieldName, bucket.bucketName());
- cursor.setString(accountFieldName, bucket.cloudAccount().value());
- cursor.setLong(updatedAtFieldName, bucket.updatedAt().toEpochMilli());
- });
-
- return slime;
- }
-
- public static ArchiveBuckets fromSlime(Slime slime) {
- Inspector inspector = slime.get();
- return new ArchiveBuckets(
- SlimeUtils.entriesStream(inspector.field(vespaManagedBucketsFieldName))
- .map(ArchiveBucketsSerializer::vespaManagedArchiveBucketFromInspector)
- .collect(Collectors.toUnmodifiableSet()),
- SlimeUtils.entriesStream(inspector.field(tenantManagedBucketsFieldName))
- .map(ArchiveBucketsSerializer::tenantManagedArchiveBucketFromInspector)
- .collect(Collectors.toUnmodifiableSet()));
- }
-
- private static VespaManagedArchiveBucket vespaManagedArchiveBucketFromInspector(Inspector inspector) {
- Set<TenantName> tenants = SlimeUtils.entriesStream(inspector.field(tenantsFieldName))
- .map(i -> TenantName.from(i.asString()))
- .collect(Collectors.toUnmodifiableSet());
-
- return new VespaManagedArchiveBucket(
- inspector.field(bucketNameFieldName).asString(),
- inspector.field(keyArnFieldName).asString())
- .withTenants(tenants);
- }
-
- private static TenantManagedArchiveBucket tenantManagedArchiveBucketFromInspector(Inspector inspector) {
- return new TenantManagedArchiveBucket(
- inspector.field(bucketNameFieldName).asString(),
- CloudAccount.from(inspector.field(accountFieldName).asString()),
- SlimeUtils.instant(inspector.field(updatedAtFieldName)));
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializer.java
deleted file mode 100644
index 92766ed4506..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializer.java
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLog;
-
-import java.nio.charset.StandardCharsets;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Slime serializer for {@link AuditLog}.
- *
- * @author mpolden
- */
-public class AuditLogSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String entriesField = "entries";
- private static final String atField = "at";
- private static final String principalField = "principal";
- private static final String methodField = "method";
- private static final String resourceField = "resource";
- private static final String dataField = "data";
- private static final String clientField = "client";
-
- public Slime toSlime(AuditLog log) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor entryArray = root.setArray(entriesField);
- log.entries().forEach(entry -> {
- Cursor entryObject = entryArray.addObject();
- entryObject.setLong(atField, entry.at().toEpochMilli());
- entryObject.setString(clientField, asString(entry.client()));
- entryObject.setString(principalField, entry.principal());
- entryObject.setString(methodField, asString(entry.method()));
- entryObject.setString(resourceField, entry.resource());
- entry.data().ifPresent(data -> entryObject.setString(dataField, data));
- });
- return slime;
- }
- public AuditLog fromSlime(Slime slime) {
- List<AuditLog.Entry> entries = new ArrayList<>();
- Cursor root = slime.get();
- root.field(entriesField).traverse((ArrayTraverser) (i, entryObject) -> {
- entries.add(new AuditLog.Entry(
- SlimeUtils.instant(entryObject.field(atField)),
- SlimeUtils.optionalString(entryObject.field(clientField))
- .map(AuditLogSerializer::clientFrom)
- .orElse(AuditLog.Entry.Client.other),
- entryObject.field(principalField).asString(),
- methodFrom(entryObject.field(methodField)),
- entryObject.field(resourceField).asString(),
- SlimeUtils.optionalString(entryObject.field(dataField))
- .map(s -> s.getBytes(StandardCharsets.UTF_8))
- .orElseGet(() -> new byte[0])
- ));
- });
- return new AuditLog(entries);
- }
-
- private static String asString(AuditLog.Entry.Method method) {
- return switch (method) {
- case POST -> "POST";
- case PATCH -> "PATCH";
- case PUT -> "PUT";
- case DELETE -> "DELETE";
- };
- }
-
- private static AuditLog.Entry.Method methodFrom(Inspector field) {
- return switch (field.asString()) {
- case "POST" -> AuditLog.Entry.Method.POST;
- case "PATCH" -> AuditLog.Entry.Method.PATCH;
- case "PUT" -> AuditLog.Entry.Method.PUT;
- case "DELETE" -> AuditLog.Entry.Method.DELETE;
- default -> throw new IllegalArgumentException("Unknown serialized value '" + field.asString() + "'");
- };
- }
-
- private static String asString(AuditLog.Entry.Client client) {
- return switch (client) {
- case console -> "console";
- case cli -> "cli";
- case hv -> "hv";
- case other -> "other";
- };
- }
-
- private static AuditLog.Entry.Client clientFrom(String s) {
- return switch (s) {
- case "console" -> AuditLog.Entry.Client.console;
- case "cli" -> AuditLog.Entry.Client.cli;
- case "hv" -> AuditLog.Entry.Client.hv;
- case "other" -> AuditLog.Entry.Client.other;
- default -> throw new IllegalArgumentException("Unknown serialized value '" + s + "'");
- };
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java
deleted file mode 100644
index 9e202ea30f2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStore.java
+++ /dev/null
@@ -1,151 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.RunDataStore;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
-import com.yahoo.vespa.hosted.controller.deployment.RunLog;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * Stores logs in bite-sized chunks using a {@link CuratorDb}, and flushes to a
- * {@link com.yahoo.vespa.hosted.controller.api.integration.RunDataStore} when the log is final.
- *
- * @author jonmv
- */
-public class BufferedLogStore {
-
- private static final int defaultChunkSize = 1 << 13; // 8kb per node stored in ZK.
- private static final int defaultMaxLogSize = 1 << 26; // 64Mb max per run.
-
- private final int chunkSize;
- private final int maxLogSize;
- private final CuratorDb buffer;
- private final RunDataStore store;
- private final LogSerializer logSerializer = new LogSerializer();
-
- public BufferedLogStore(CuratorDb buffer, RunDataStore store) {
- this(defaultChunkSize, defaultMaxLogSize, buffer, store);
- }
-
- BufferedLogStore(int chunkSize, int maxLogSize, CuratorDb buffer, RunDataStore store) {
- this.chunkSize = chunkSize;
- this.maxLogSize = maxLogSize;
- this.buffer = buffer;
- this.store = store;
- }
-
- /** Appends to the log of the given, active run, reassigning IDs as counted here, and converting to Vespa log levels. */
- public void append(ApplicationId id, JobType type, Step step, List<LogEntry> entries, boolean forceLog) {
- if (entries.isEmpty())
- return;
-
- // Start a new chunk if the previous one is full, or if none have been written yet.
- // The id of a chunk is the id of the first entry in it.
- long lastEntryId = buffer.readLastLogEntryId(id, type).orElse(-1L);
- long lastChunkId = buffer.getLogChunkIds(id, type).max().orElse(0);
- long numberOfChunks = Math.max(1, buffer.getLogChunkIds(id, type).count());
- if (numberOfChunks > maxLogSize / chunkSize && ! forceLog)
- return; // Max size exceeded — store no more.
-
- byte[] emptyChunk = "[]".getBytes();
- byte[] lastChunk = buffer.readLog(id, type, lastChunkId).filter(chunk -> chunk.length > 0).orElse(emptyChunk);
-
- long sizeLowerBound = lastChunk.length;
- Map<Step, List<LogEntry>> log = logSerializer.fromJson(lastChunk, -1);
- List<LogEntry> stepEntries = log.computeIfAbsent(step, __ -> new ArrayList<>());
- for (LogEntry entry : entries) {
- if (sizeLowerBound > chunkSize) {
- buffer.writeLog(id, type, lastChunkId, logSerializer.toJson(log));
- buffer.writeLastLogEntryId(id, type, lastEntryId);
- lastChunkId = lastEntryId + 1;
- if (++numberOfChunks > maxLogSize / chunkSize && ! forceLog) {
- log = Map.of(step, List.of(new LogEntry(++lastEntryId,
- entry.at(),
- LogEntry.Type.warning,
- "Max log size of " + (maxLogSize >> 20) +
- "Mb exceeded; further user entries are discarded.")));
- break;
- }
- log = new HashMap<>();
- log.put(step, stepEntries = new ArrayList<>());
- sizeLowerBound = 2;
- }
- stepEntries.add(new LogEntry(++lastEntryId, entry.at(), entry.type(), entry.message()));
- sizeLowerBound += entry.message().length();
- }
- buffer.writeLog(id, type, lastChunkId, logSerializer.toJson(log));
- buffer.writeLastLogEntryId(id, type, lastEntryId);
- }
-
- /** Reads all log entries after the given threshold, from the buffered log, i.e., for an active run. */
- public RunLog readActive(ApplicationId id, JobType type, long after) {
- return buffer.readLastLogEntryId(id, type).orElse(-1L) <= after
- ? RunLog.empty()
- : RunLog.of(readChunked(id, type, after));
- }
-
- /** Reads all log entries after the given threshold, from the stored log, i.e., for a finished run. */
- public Optional<RunLog> readFinished(RunId id, long after) {
- return store.get(id).map(json -> RunLog.of(logSerializer.fromJson(json, after)));
- }
-
- /** Writes the buffered log of the now finished run to the long-term store, and clears the buffer. */
- public void flush(RunId id) {
- store.put(id, logSerializer.toJson(readChunked(id.application(), id.type(), -1)));
- buffer.deleteLog(id.application(), id.type());
- }
-
- /** Deletes the logs for the given run, if already moved to storage. */
- public void delete(RunId id) {
- store.delete(id);
- }
-
- /** Deletes all logs in permanent storage for the given application. */
- public void delete(ApplicationId id) {
- store.delete(id);
- }
-
- private Map<Step, List<LogEntry>> readChunked(ApplicationId id, JobType type, long after) {
- long[] chunkIds = buffer.getLogChunkIds(id, type).toArray();
- int firstChunk = chunkIds.length;
- while (firstChunk > 0 && chunkIds[--firstChunk] > after + 1);
- return logSerializer.fromJson(Arrays.stream(chunkIds, firstChunk, chunkIds.length)
- .mapToObj(chunkId -> buffer.readLog(id, type, chunkId))
- .flatMap(Optional::stream)
- .toList(),
- after);
- }
-
- public Optional<String> readTestReports(RunId id) {
- return store.getTestReport(id).map(bytes -> "[" + new String(bytes, UTF_8) + "]");
- }
-
- public void writeTestReport(RunId id, TestReport report) {
- byte[] bytes = report.toJson().getBytes(UTF_8);
- Optional<byte[]> existing = store.getTestReport(id);
- if (existing.isPresent()) {
- byte[] aggregate = new byte[existing.get().length + 1 + bytes.length];
- System.arraycopy(existing.get(), 0, aggregate, 0, existing.get().length);
- aggregate[existing.get().length] = ',';
- System.arraycopy(bytes, 0, aggregate, existing.get().length + 1, bytes.length);
- bytes = aggregate;
- }
- store.putTestReport(id, bytes);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CertifiedOsVersionSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CertifiedOsVersionSerializer.java
deleted file mode 100644
index f3b3cb0a1bf..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CertifiedOsVersionSerializer.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.versions.CertifiedOsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Serializer for {@link com.yahoo.vespa.hosted.controller.versions.CertifiedOsVersion}.
- *
- * @author mpolden
- */
-public class CertifiedOsVersionSerializer {
-
- private static final String versionField = "version";
- private static final String cloudField = "cloud";
- private static final String vespaVersionField = "vespaVersion";
-
- public Slime toSlime(Set<CertifiedOsVersion> versions) {
- Slime slime = new Slime();
- Cursor array = slime.setArray();
- for (var version : versions) {
- Cursor root = array.addObject();
- root.setString(versionField, version.osVersion().version().toFullString());
- root.setString(cloudField, version.osVersion().cloud().value());
- root.setString(vespaVersionField, version.vespaVersion().toFullString());
- }
- return slime;
- }
-
- public Set<CertifiedOsVersion> fromSlime(Slime slime) {
- Cursor array = slime.get();
- Set<CertifiedOsVersion> certifiedOsVersions = new HashSet<>();
- array.traverse((ArrayTraverser) (idx, object) -> certifiedOsVersions.add(
- new CertifiedOsVersion(new OsVersion(Version.fromString(object.field(versionField).asString()),
- CloudName.from(object.field(cloudField).asString())),
- Version.fromString(object.field(vespaVersionField).asString())))
- );
- return Collections.unmodifiableSet(certifiedOsVersions);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializer.java
deleted file mode 100644
index f43be77b82c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializer.java
+++ /dev/null
@@ -1,157 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
-
-import java.time.Instant;
-import java.time.ZonedDateTime;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * @author olaa
- */
-public class ChangeRequestSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String ID_FIELD = "id";
- private static final String SOURCE_FIELD = "source";
- private static final String SOURCE_SYSTEM_FIELD = "system";
- private static final String STATUS_FIELD = "status";
- private static final String URL_FIELD = "url";
- private static final String ZONE_FIELD = "zoneId";
- private static final String START_TIME_FIELD = "plannedStartTime";
- private static final String END_TIME_FIELD = "plannedEndTime";
- private static final String APPROVAL_FIELD = "approval";
- private static final String IMPACT_FIELD = "impact";
- private static final String IMPACTED_HOSTS_FIELD = "impactedHosts";
- private static final String IMPACTED_SWITCHES_FIELD = "impactedSwitches";
- private static final String ACTION_PLAN_FIELD = "actionPlan";
- private static final String HOST_FIELD = "hostname";
- private static final String ACTION_STATE_FIELD = "state";
- private static final String LAST_UPDATED_FIELD = "lastUpdated";
- private static final String HOSTS_FIELD = "hosts";
- private static final String CATEGORY_FIELD = "category";
-
-
- public static VespaChangeRequest fromSlime(Slime slime) {
- var inspector = slime.get();
- var id = inspector.field(ID_FIELD).asString();
- var zoneId = ZoneId.from(inspector.field(ZONE_FIELD).asString());
- var changeRequestSource = readChangeRequestSource(inspector.field(SOURCE_FIELD));
- var actionPlan = readHostActionPlan(inspector.field(ACTION_PLAN_FIELD));
- var status = VespaChangeRequest.Status.valueOf(inspector.field(STATUS_FIELD).asString());
- var impact = ChangeRequest.Impact.valueOf(inspector.field(IMPACT_FIELD).asString());
- var approval = ChangeRequest.Approval.valueOf(inspector.field(APPROVAL_FIELD).asString());
- var category = inspector.field(CATEGORY_FIELD).valid() ?
- inspector.field(CATEGORY_FIELD).asString() : "Unknown";
-
- var impactedHosts = new ArrayList<String>();
- inspector.field(IMPACTED_HOSTS_FIELD)
- .traverse((ArrayTraverser) (i, hostname) -> impactedHosts.add(hostname.asString()));
- var impactedSwitches = new ArrayList<String>();
- inspector.field(IMPACTED_SWITCHES_FIELD)
- .traverse((ArrayTraverser) (i, switchName) -> impactedSwitches.add(switchName.asString()));
-
- return new VespaChangeRequest(
- id,
- changeRequestSource,
- impactedSwitches,
- impactedHosts,
- approval,
- impact,
- status,
- actionPlan,
- zoneId);
- }
-
- public static Slime toSlime(VespaChangeRequest changeRequest) {
- var slime = new Slime();
- writeChangeRequest(slime.setObject(), changeRequest);
- return slime;
- }
-
- public static void writeChangeRequest(Cursor cursor, VespaChangeRequest changeRequest) {
- cursor.setString(ID_FIELD, changeRequest.getId());
- cursor.setString(STATUS_FIELD, changeRequest.getStatus().name());
- cursor.setString(IMPACT_FIELD, changeRequest.getImpact().name());
- cursor.setString(APPROVAL_FIELD, changeRequest.getApproval().name());
- cursor.setString(ZONE_FIELD, changeRequest.getZoneId().value());
- writeChangeRequestSource(cursor.setObject(SOURCE_FIELD), changeRequest.getChangeRequestSource());
- writeActionPlan(cursor.setObject(ACTION_PLAN_FIELD), changeRequest);
-
- var impactedHosts = cursor.setArray(IMPACTED_HOSTS_FIELD);
- changeRequest.getImpactedHosts().forEach(impactedHosts::addString);
- var impactedSwitches = cursor.setArray(IMPACTED_SWITCHES_FIELD);
- changeRequest.getImpactedSwitches().forEach(impactedSwitches::addString);
- }
-
- private static void writeActionPlan(Cursor cursor, VespaChangeRequest changeRequest) {
- var hostsCursor = cursor.setArray(HOSTS_FIELD);
-
- changeRequest.getHostActionPlan().forEach(action -> {
- var actionCursor = hostsCursor.addObject();
- actionCursor.setString(HOST_FIELD, action.getHostname());
- actionCursor.setString(ACTION_STATE_FIELD, action.getState().name());
- actionCursor.setString(LAST_UPDATED_FIELD, action.getLastUpdated().toString());
- });
-
- // TODO: Add action plan per application
- }
-
- private static void writeChangeRequestSource(Cursor cursor, ChangeRequestSource source) {
- cursor.setString(SOURCE_SYSTEM_FIELD, source.system());
- cursor.setString(ID_FIELD, source.id());
- cursor.setString(URL_FIELD, source.url());
- cursor.setString(START_TIME_FIELD, source.plannedStartTime().toString());
- cursor.setString(END_TIME_FIELD, source.plannedEndTime().toString());
- cursor.setString(STATUS_FIELD, source.status().name());
- cursor.setString(CATEGORY_FIELD, source.category());
- }
-
- public static ChangeRequestSource readChangeRequestSource(Inspector inspector) {
- var category = inspector.field(CATEGORY_FIELD).valid() ?
- inspector.field(CATEGORY_FIELD).asString() : "Unknown";
- return new ChangeRequestSource(
- inspector.field(SOURCE_SYSTEM_FIELD).asString(),
- inspector.field(ID_FIELD).asString(),
- inspector.field(URL_FIELD).asString(),
- ChangeRequestSource.Status.valueOf(inspector.field(STATUS_FIELD).asString()),
- ZonedDateTime.parse(inspector.field(START_TIME_FIELD).asString()),
- ZonedDateTime.parse(inspector.field(END_TIME_FIELD).asString()),
- category
- );
- }
-
- public static List<HostAction> readHostActionPlan(Inspector inspector) {
- if (!inspector.valid())
- return List.of();
-
- var actionPlan = new ArrayList<HostAction>();
- inspector.field(HOSTS_FIELD).traverse((ArrayTraverser) (index, hostObject) ->
- actionPlan.add(
- new HostAction(
- hostObject.field(HOST_FIELD).asString(),
- HostAction.State.valueOf(hostObject.field(ACTION_STATE_FIELD).asString()),
- Instant.parse(hostObject.field(LAST_UPDATED_FIELD).asString())
- )
- )
- );
- return actionPlan;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ConfidenceOverrideSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ConfidenceOverrideSerializer.java
deleted file mode 100644
index 91e12b9cb15..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ConfidenceOverrideSerializer.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.ObjectTraverser;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Serializer for {@link Confidence} overrides.
- *
- * @author mpolden
- */
-public class ConfidenceOverrideSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private final static String overridesField = "overrides";
-
- public Slime toSlime(Map<Version, Confidence> overrides) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor object = root.setObject(overridesField);
- overrides.forEach((version, confidence) -> object.setString(version.toString(), confidence.name()));
- return slime;
- }
-
- public Map<Version, Confidence> fromSlime(Slime slime) {
- Cursor root = slime.get();
- Cursor overridesObject = root.field(overridesField);
- Map<Version, Confidence> overrides = new LinkedHashMap<>();
- overridesObject.traverse((ObjectTraverser) (name, value) -> {
- overrides.put(Version.fromString(name), Confidence.valueOf(value.asString()));
- });
- return Collections.unmodifiableMap(overrides);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java
deleted file mode 100644
index f19d7f68b3d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializer.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
-
-/**
- * Serializer for {@link ControllerVersion}.
- *
- * @author mpolden
- */
-public class ControllerVersionSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String VERSION_FIELD = "version";
- private static final String COMMIT_SHA_FIELD = "commitSha";
- private static final String COMMIT_DATE_FIELD = "commitDate";
-
- public Slime toSlime(ControllerVersion controllerVersion) {
- var slime = new Slime();
- var root = slime.setObject();
- root.setString(VERSION_FIELD, controllerVersion.version().toFullString());
- root.setString(COMMIT_SHA_FIELD, controllerVersion.commitSha());
- root.setLong(COMMIT_DATE_FIELD, controllerVersion.commitDate().toEpochMilli());
- return slime;
- }
-
- public ControllerVersion fromSlime(Slime slime) {
- var root = slime.get();
- var version = Version.fromString(root.field(VERSION_FIELD).asString());
- var commitSha = root.field(COMMIT_SHA_FIELD).asString();
- var commitDate = SlimeUtils.instant(root.field(COMMIT_DATE_FIELD));
- return new ControllerVersion(version, commitSha, commitDate);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
deleted file mode 100644
index cef62438a53..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/CuratorDb.java
+++ /dev/null
@@ -1,948 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.collections.Pair;
-import com.yahoo.component.Version;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.concurrent.UncheckedTimeoutException;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec.Id;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.path.Path;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.transaction.NestedTransaction;
-import com.yahoo.vespa.curator.Curator;
-import com.yahoo.vespa.curator.transaction.CuratorOperation;
-import com.yahoo.vespa.curator.transaction.CuratorOperations;
-import com.yahoo.vespa.curator.transaction.CuratorTransaction;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService.DnsChallenge;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLog;
-import com.yahoo.vespa.hosted.controller.certificate.AssignedCertificate;
-import com.yahoo.vespa.hosted.controller.certificate.UnassignedCertificate;
-import com.yahoo.vespa.hosted.controller.deployment.RetriggerEntry;
-import com.yahoo.vespa.hosted.controller.deployment.RetriggerEntrySerializer;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue;
-import com.yahoo.vespa.hosted.controller.notification.Notification;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicy;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-import com.yahoo.vespa.hosted.controller.routing.ZoneRoutingPolicy;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccess;
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.versions.CertifiedOsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.ByteBuffer;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.NavigableMap;
-import java.util.Optional;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.LongStream;
-
-import static java.util.stream.Collectors.collectingAndThen;
-
-/**
- * Curator backed database for storing the persistence state of controllers. This maps controller specific operations
- * to general curator operations.
- *
- * @author bratseth
- * @author mpolden
- * @author jonmv
- */
-public class CuratorDb {
-
- private static final Logger log = Logger.getLogger(CuratorDb.class.getName());
- private static final Duration deployLockTimeout = Duration.ofMinutes(30);
- private static final Duration defaultLockTimeout = Duration.ofMinutes(5);
- private static final Duration defaultTryLockTimeout = Duration.ofSeconds(1);
-
- private static final Path root = Path.fromString("/controller/v1");
-
- private static final Path lockRoot = root.append("locks");
-
- private static final Path tenantRoot = root.append("tenants");
- private static final Path applicationRoot = root.append("applications");
- private static final Path jobRoot = root.append("jobs");
- private static final Path controllerRoot = root.append("controllers");
- private static final Path routingPoliciesRoot = root.append("routingPolicies");
- private static final Path dnsChallengesRoot = root.append("dnsChallenges");
- private static final Path zoneRoutingPoliciesRoot = root.append("zoneRoutingPolicies");
- private static final Path endpointCertificateRoot = root.append("applicationCertificates");
- private static final Path archiveBucketsRoot = root.append("archiveBuckets");
- private static final Path changeRequestsRoot = root.append("changeRequests");
- private static final Path notificationsRoot = root.append("notifications");
- private static final Path supportAccessRoot = root.append("supportAccess");
- private static final Path mailVerificationRoot = root.append("mailVerification");
- private static final Path dataPlaneTokenRoot = root.append("dataplaneTokens");
- private static final Path certificatePoolRoot = root.append("certificatePool");
- private static final Path trialNotificationsRoot = root.append("trialNotifications");
-
- private final NodeVersionSerializer nodeVersionSerializer = new NodeVersionSerializer();
- private final VersionStatusSerializer versionStatusSerializer = new VersionStatusSerializer(nodeVersionSerializer);
- private final ControllerVersionSerializer controllerVersionSerializer = new ControllerVersionSerializer();
- private final ConfidenceOverrideSerializer confidenceOverrideSerializer = new ConfidenceOverrideSerializer();
- private final TenantSerializer tenantSerializer = new TenantSerializer();
- private final OsVersionSerializer osVersionSerializer = new OsVersionSerializer();
- private final OsVersionTargetSerializer osVersionTargetSerializer = new OsVersionTargetSerializer(osVersionSerializer);
- private final OsVersionStatusSerializer osVersionStatusSerializer = new OsVersionStatusSerializer(osVersionSerializer, nodeVersionSerializer);
- private final CertifiedOsVersionSerializer certifiedOsVersionSerializer = new CertifiedOsVersionSerializer();
- private final RoutingPolicySerializer routingPolicySerializer = new RoutingPolicySerializer();
- private final ZoneRoutingPolicySerializer zoneRoutingPolicySerializer = new ZoneRoutingPolicySerializer(routingPolicySerializer);
- private final AuditLogSerializer auditLogSerializer = new AuditLogSerializer();
- private final NameServiceQueueSerializer nameServiceQueueSerializer = new NameServiceQueueSerializer();
- private final ApplicationSerializer applicationSerializer = new ApplicationSerializer();
- private final RunSerializer runSerializer = new RunSerializer();
- private final RetriggerEntrySerializer retriggerEntrySerializer = new RetriggerEntrySerializer();
- private final NotificationsSerializer notificationsSerializer = new NotificationsSerializer();
- private final DnsChallengeSerializer dnsChallengeSerializer = new DnsChallengeSerializer();
- private final UnassignedCertificateSerializer unassignedCertificateSerializer = new UnassignedCertificateSerializer();
-
- private final Curator curator;
- private final Duration tryLockTimeout;
-
- // For each application id (path), store the ZK node version and its deserialised data - update when version changes.
- // This will grow to keep all applications in memory, but this should be OK
- private final Map<Path, Pair<Integer, Application>> cachedApplications = new ConcurrentHashMap<>();
-
- // For each job id (path), store the ZK node version and its deserialised data - update when version changes.
- private final Map<Path, Pair<Integer, NavigableMap<RunId, Run>>> cachedHistoricRuns = new ConcurrentHashMap<>();
-
- // Store the ZK node version and its deserialised data - update when version changes.
- private final AtomicReference<Pair<Integer, VersionStatus>> cachedVersionStatus = new AtomicReference<>();
-
- @Inject
- public CuratorDb(Curator curator) {
- this(curator, defaultTryLockTimeout);
- }
-
- CuratorDb(Curator curator, Duration tryLockTimeout) {
- this.curator = curator;
- this.tryLockTimeout = tryLockTimeout;
- }
-
- /** Returns all hostnames configured to be part of this ZooKeeper cluster */
- public List<String> cluster() {
- return Arrays.stream(curator.zooKeeperEnsembleConnectionSpec().split(","))
- .filter(hostAndPort -> !hostAndPort.isEmpty())
- .map(hostAndPort -> hostAndPort.split(":")[0])
- .toList();
- }
-
- // -------------- Locks ---------------------------------------------------
-
- public Mutex lock(TenantName name) {
- return curator.lock(lockRoot.append("tenants").append(name.value()), defaultLockTimeout.multipliedBy(2));
- }
-
- public Mutex lock(TenantAndApplicationId id) {
- return curator.lock(lockRoot.append("applications").append(id.tenant().value() + ":" +
- id.application().value()),
- defaultLockTimeout.multipliedBy(2));
- }
-
- public Mutex lockForDeployment(ApplicationId id, ZoneId zone) {
- return curator.lock(lockRoot.append("instances").append(id.serializedForm() + ":" + zone.environment().value() +
- ":" + zone.region().value()),
- deployLockTimeout);
- }
-
- public Mutex lock(ApplicationId id, JobType type) {
- return curator.lock(lockRoot.append("jobs").append(id.serializedForm() + ":" + type.jobName()),
- defaultLockTimeout);
- }
-
- public Mutex lock(ApplicationId id, JobType type, Step step) throws TimeoutException {
- return tryLock(lockRoot.append("steps").append(id.serializedForm() + ":" + type.jobName() + ":" + step.name()));
- }
-
- public Mutex lockRotations() {
- return curator.lock(lockRoot.append("rotations"), defaultLockTimeout);
- }
-
- public Mutex lockConfidenceOverrides() {
- return curator.lock(lockRoot.append("confidenceOverrides"), defaultLockTimeout);
- }
-
- public Mutex lockMaintenanceJob(String jobName) {
- try {
- return tryLock(lockRoot.append("maintenanceJobLocks").append(jobName));
- } catch (TimeoutException e) {
- throw new UncheckedTimeoutException(e);
- }
- }
-
- public Mutex lockProvisionState(String provisionStateId) {
- return curator.lock(lockRoot.append("provisioning").append("states").append(provisionStateId), Duration.ofSeconds(1));
- }
-
- public Mutex lockOsVersions() {
- return curator.lock(lockRoot.append("osTargetVersion"), defaultLockTimeout);
- }
-
- public Mutex lockOsVersionStatus() {
- return curator.lock(lockRoot.append("osVersionStatus"), defaultLockTimeout);
- }
-
- public Mutex lockCertifiedOsVersions() {
- return curator.lock(lockRoot.append("certifiedOsVersions"), defaultLockTimeout);
- }
-
- public Mutex lockRoutingPolicies() {
- return curator.lock(lockRoot.append("routingPolicies"), defaultLockTimeout);
- }
-
- public Mutex lockAuditLog() {
- return curator.lock(lockRoot.append("auditLog"), defaultLockTimeout);
- }
-
- public Mutex lockNameServiceQueue() {
- return curator.lock(lockRoot.append("nameServiceQueue"), defaultLockTimeout);
- }
-
- public Mutex lockMeteringRefreshTime() throws TimeoutException {
- return tryLock(lockRoot.append("meteringRefreshTime"));
- }
-
- public Mutex lockArchiveBuckets(ZoneId zoneId) {
- return curator.lock(lockRoot.append("archiveBuckets").append(zoneId.value()), defaultLockTimeout);
- }
-
- public Mutex lockChangeRequests() {
- return curator.lock(lockRoot.append("changeRequests"), defaultLockTimeout);
- }
-
- public Mutex lockNotifications(TenantName tenantName) {
- return curator.lock(lockRoot.append("notifications").append(tenantName.value()), defaultLockTimeout);
- }
-
- public Mutex lockSupportAccess(DeploymentId deploymentId) {
- return curator.lock(lockRoot.append("supportAccess").append(deploymentId.dottedString()), defaultLockTimeout);
- }
-
- public Mutex lockDeploymentRetriggerQueue() {
- return curator.lock(lockRoot.append("deploymentRetriggerQueue"), defaultLockTimeout);
- }
-
- public Mutex lockPendingMailVerification(String verificationCode) {
- return curator.lock(lockRoot.append("pendingMailVerification").append(verificationCode), defaultLockTimeout);
- }
-
- public Mutex lockCertificatePool() {
- return curator.lock(lockRoot.append("certificatePool"), defaultLockTimeout);
- }
-
- // -------------- Helpers ------------------------------------------
-
- /** Try locking with a low timeout, meaning it is OK to fail lock acquisition.
- *
- * Useful for maintenance jobs, where there is no point in running the jobs back to back.
- */
- private Mutex tryLock(Path path) throws TimeoutException {
- try {
- return curator.lock(path, tryLockTimeout);
- }
- catch (UncheckedTimeoutException e) {
- throw new TimeoutException(e.getMessage());
- }
- }
-
- private <T> Optional<T> read(Path path, Function<byte[], T> mapper) {
- return curator.getData(path).filter(data -> data.length > 0).map(mapper);
- }
-
- private Optional<Slime> readSlime(Path path) {
- return read(path, SlimeUtils::jsonToSlime);
- }
-
- private static byte[] asJson(Slime slime) {
- try {
- return SlimeUtils.toJsonBytes(slime);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- // -------------- Deployment orchestration --------------------------------
-
- public double readUpgradesPerMinute() {
- return read(upgradesPerMinutePath(), ByteBuffer::wrap).map(ByteBuffer::getDouble).orElse(0.125);
- }
-
- public void writeUpgradesPerMinute(double n) {
- curator.set(upgradesPerMinutePath(), ByteBuffer.allocate(Double.BYTES).putDouble(n).array());
- }
-
- public void writeVersionStatus(VersionStatus status) {
- curator.set(versionStatusPath(), asJson(versionStatusSerializer.toSlime(status)));
- }
-
- public VersionStatus readVersionStatus() {
- Path path = versionStatusPath();
- return curator.getStat(path)
- .map(stat -> cachedVersionStatus.updateAndGet(old ->
- old != null && old.getFirst() == stat.getVersion()
- ? old
- : new Pair<>(stat.getVersion(), read(path, bytes -> versionStatusSerializer.fromSlime(SlimeUtils.jsonToSlime(bytes))).get())).getSecond())
- .orElseGet(VersionStatus::empty);
- }
-
- public void writeConfidenceOverrides(Map<Version, VespaVersion.Confidence> overrides) {
- curator.set(confidenceOverridesPath(), asJson(confidenceOverrideSerializer.toSlime(overrides)));
- }
-
- public Map<Version, VespaVersion.Confidence> readConfidenceOverrides() {
- return readSlime(confidenceOverridesPath()).map(confidenceOverrideSerializer::fromSlime)
- .orElseGet(Collections::emptyMap);
- }
-
- public void writeControllerVersion(HostName hostname, ControllerVersion version) {
- curator.set(controllerPath(hostname.value()), asJson(controllerVersionSerializer.toSlime(version)));
- }
-
- public ControllerVersion readControllerVersion(HostName hostname) {
- return readSlime(controllerPath(hostname.value()))
- .map(controllerVersionSerializer::fromSlime)
- .orElse(ControllerVersion.CURRENT);
- }
-
- // OS upgrades
-
- public void writeOsVersionTargets(SortedSet<OsVersionTarget> versions) {
- curator.set(osVersionTargetsPath(), asJson(osVersionTargetSerializer.toSlime(versions)));
- }
-
- public Set<OsVersionTarget> readOsVersionTargets() {
- return readSlime(osVersionTargetsPath()).map(osVersionTargetSerializer::fromSlime).orElseGet(Collections::emptySet);
- }
-
- public void writeOsVersionStatus(OsVersionStatus status) {
- curator.set(osVersionStatusPath(), asJson(osVersionStatusSerializer.toSlime(status)));
- }
-
- public OsVersionStatus readOsVersionStatus() {
- return readSlime(osVersionStatusPath()).map(osVersionStatusSerializer::fromSlime).orElse(OsVersionStatus.empty);
- }
-
- public void writeCertifiedOsVersions(Set<CertifiedOsVersion> certifiedOsVersions) {
- curator.set(certifiedOsVersionsPath(), asJson(certifiedOsVersionSerializer.toSlime(certifiedOsVersions)));
- }
-
- public Set<CertifiedOsVersion> readCertifiedOsVersions() {
- return readSlime(certifiedOsVersionsPath()).map(certifiedOsVersionSerializer::fromSlime).orElseGet(Set::of);
- }
-
- // -------------- Tenant --------------------------------------------------
-
- public void writeTenant(Tenant tenant) {
- curator.set(tenantPath(tenant.name()), asJson(tenantSerializer.toSlime(tenant)));
- }
-
- public Optional<Tenant> readTenant(TenantName name) {
- return readSlime(tenantPath(name)).map(tenantSerializer::tenantFrom);
- }
-
- public List<Tenant> readTenants() {
- return readTenantNames().stream()
- .map(this::readTenant)
- .flatMap(Optional::stream)
- .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
- }
-
- public List<TenantName> readTenantNames() {
- return curator.getChildren(tenantRoot).stream()
- .map(TenantName::from)
- .toList();
- }
-
- public void removeTenant(TenantName name) {
- curator.delete(tenantPath(name));
- }
-
- // -------------- Applications ---------------------------------------------
-
- public void writeApplication(Application application) {
- curator.set(applicationPath(application.id()), asJson(applicationSerializer.toSlime(application)));
- }
-
- public Optional<Application> readApplication(TenantAndApplicationId application) {
- Path path = applicationPath(application);
- return curator.getStat(path)
- .map(stat -> cachedApplications.compute(path, (__, old) ->
- old != null && old.getFirst() == stat.getVersion()
- ? old
- : new Pair<>(stat.getVersion(), read(path, applicationSerializer::fromSlime).get())).getSecond());
- }
-
- public List<Application> readApplications(boolean canFail) {
- return readApplications(ignored -> true, canFail);
- }
-
- public List<Application> readApplications(TenantName name) {
- return readApplications(application -> application.tenant().equals(name), false);
- }
-
- private List<Application> readApplications(Predicate<TenantAndApplicationId> applicationFilter, boolean canFail) {
- var applicationIds = readApplicationIds();
- var applications = new ArrayList<Application>(applicationIds.size());
- for (var id : applicationIds) {
- if (!applicationFilter.test(id)) continue;
- try {
- readApplication(id).ifPresent(applications::add);
- } catch (Exception e) {
- if (canFail) {
- log.log(Level.SEVERE, "Failed to read application '" + id + "', this must be fixed through " +
- "manual intervention", e);
- } else {
- throw e;
- }
- }
- }
- return Collections.unmodifiableList(applications);
- }
-
- public List<TenantAndApplicationId> readApplicationIds() {
- return curator.getChildren(applicationRoot).stream()
- .map(TenantAndApplicationId::fromSerialized)
- .sorted()
- .toList();
- }
-
- public void removeApplication(TenantAndApplicationId id) {
- curator.delete(applicationPath(id));
- }
-
- // -------------- Job Runs ------------------------------------------------
-
- public void writeLastRun(Run run) {
- curator.set(lastRunPath(run.id().application(), run.id().type()), asJson(runSerializer.toSlime(run)));
- }
-
- public void writeHistoricRuns(ApplicationId id, JobType type, Iterable<Run> runs) {
- Path path = runsPath(id, type);
- curator.set(path, asJson(runSerializer.toSlime(runs)));
- }
-
- public Optional<Run> readLastRun(ApplicationId id, JobType type) {
- return readSlime(lastRunPath(id, type)).map(runSerializer::runFromSlime);
- }
-
- public NavigableMap<RunId, Run> readHistoricRuns(ApplicationId id, JobType type) {
- Path path = runsPath(id, type);
- return curator.getStat(path)
- .map(stat -> cachedHistoricRuns.compute(path, (__, old) ->
- old != null && old.getFirst() == stat.getVersion()
- ? old
- : new Pair<>(stat.getVersion(), runSerializer.runsFromSlime(readSlime(path).get()))).getSecond())
- .orElseGet(Collections::emptyNavigableMap);
- }
-
- public void deleteRunData(ApplicationId id, JobType type) {
- curator.delete(runsPath(id, type));
- curator.delete(lastRunPath(id, type));
- }
-
- public void deleteRunData(ApplicationId id) {
- curator.delete(jobRoot.append(id.serializedForm()));
- }
-
- public List<ApplicationId> applicationsWithJobs() {
- return curator.getChildren(jobRoot).stream()
- .map(ApplicationId::fromSerializedForm)
- .toList();
- }
-
-
- public Optional<byte[]> readLog(ApplicationId id, JobType type, long chunkId) {
- return curator.getData(logPath(id, type, chunkId));
- }
-
- public void writeLog(ApplicationId id, JobType type, long chunkId, byte[] log) {
- curator.set(logPath(id, type, chunkId), log);
- }
-
- public void deleteLog(ApplicationId id, JobType type) {
- curator.delete(runsPath(id, type).append("logs"));
- }
-
- public Optional<Long> readLastLogEntryId(ApplicationId id, JobType type) {
- return curator.getData(lastLogPath(id, type))
- .map(String::new).map(Long::parseLong);
- }
-
- public void writeLastLogEntryId(ApplicationId id, JobType type, long lastId) {
- curator.set(lastLogPath(id, type), Long.toString(lastId).getBytes());
- }
-
- public LongStream getLogChunkIds(ApplicationId id, JobType type) {
- return curator.getChildren(runsPath(id, type).append("logs")).stream()
- .mapToLong(Long::parseLong)
- .sorted();
- }
-
- // -------------- Audit log -----------------------------------------------
-
- public AuditLog readAuditLog() {
- return readSlime(auditLogPath()).map(auditLogSerializer::fromSlime)
- .orElse(AuditLog.empty);
- }
-
- public void writeAuditLog(AuditLog log) {
- curator.set(auditLogPath(), asJson(auditLogSerializer.toSlime(log)));
- }
-
-
- // -------------- Name service log ----------------------------------------
-
- public NameServiceQueue readNameServiceQueue() {
- return readSlime(nameServiceQueuePath()).map(nameServiceQueueSerializer::fromSlime)
- .orElse(NameServiceQueue.EMPTY);
- }
-
- public void writeNameServiceQueue(NameServiceQueue queue) {
- curator.set(nameServiceQueuePath(), asJson(nameServiceQueueSerializer.toSlime(queue)));
- }
-
- // -------------- Provisioning (called by internal code) ------------------
-
- @SuppressWarnings("unused")
- public Optional<byte[]> readProvisionState(String provisionId) {
- return curator.getData(provisionStatePath(provisionId));
- }
-
- @SuppressWarnings("unused")
- public void writeProvisionState(String provisionId, byte[] data) {
- curator.set(provisionStatePath(provisionId), data);
- }
-
- @SuppressWarnings("unused")
- public List<String> readProvisionStateIds() {
- return curator.getChildren(provisionStatePath());
- }
-
- // -------------- Routing policies ----------------------------------------
-
- public void writeRoutingPolicies(ApplicationId application, List<RoutingPolicy> policies) {
- for (var policy : policies) {
- if (!policy.id().owner().equals(application)) {
- throw new IllegalArgumentException(policy.id() + " does not belong to the application being written: " +
- application.toShortString());
- }
- }
- curator.set(routingPolicyPath(application), asJson(routingPolicySerializer.toSlime(policies)));
- }
-
- public Map<ApplicationId, List<RoutingPolicy>> readRoutingPolicies() {
- return readRoutingPolicies((instance) -> true);
- }
-
- public Map<ApplicationId, List<RoutingPolicy>> readRoutingPolicies(Predicate<ApplicationId> filter) {
- return curator.getChildren(routingPoliciesRoot).stream()
- .map(ApplicationId::fromSerializedForm)
- .filter(filter)
- .collect(Collectors.toUnmodifiableMap(Function.identity(),
- this::readRoutingPolicies));
- }
-
- public List<RoutingPolicy> readRoutingPolicies(ApplicationId application) {
- return readSlime(routingPolicyPath(application)).map(slime -> routingPolicySerializer.fromSlime(application, slime))
- .orElseGet(List::of);
- }
-
- public void writeZoneRoutingPolicy(ZoneRoutingPolicy policy) {
- curator.set(zoneRoutingPolicyPath(policy.zone()), asJson(zoneRoutingPolicySerializer.toSlime(policy)));
- }
-
- public ZoneRoutingPolicy readZoneRoutingPolicy(ZoneId zone) {
- return readSlime(zoneRoutingPolicyPath(zone)).map(data -> zoneRoutingPolicySerializer.fromSlime(zone, data))
- .orElseGet(() -> new ZoneRoutingPolicy(zone, RoutingStatus.DEFAULT));
- }
-
- public void writeDnsChallenge(DnsChallenge challenge) {
- curator.set(dnsChallengePath(challenge.clusterId()), dnsChallengeSerializer.toJson(challenge));
- }
-
- public void deleteDnsChallenge(ClusterId id) {
- curator.delete(dnsChallengePath(id));
- }
-
- public List<DnsChallenge> readDnsChallenges(DeploymentId id) {
- return curator.getChildren(dnsChallengePath(id)).stream()
- .map(cluster -> readDnsChallenge(new ClusterId(id, Id.from(cluster))))
- .toList();
- }
-
- private DnsChallenge readDnsChallenge(ClusterId clusterId) {
- return curator.getData(dnsChallengePath(clusterId))
- .map(bytes -> dnsChallengeSerializer.fromJson(bytes, clusterId))
- .orElseThrow(() -> new IllegalArgumentException("no DNS challenge for " + clusterId));
- }
-
- private static Path dnsChallengePath(DeploymentId id) {
- return dnsChallengesRoot.append(id.applicationId().serializedForm())
- .append(id.zoneId().value());
- }
-
- private static Path dnsChallengePath(ClusterId id) {
- return dnsChallengePath(id.deploymentId()).append(id.clusterId().value());
- }
-
- // -------------- Application endpoint certificates ----------------------------
-
- public void writeAssignedCertificate(AssignedCertificate certificate) {
- try (NestedTransaction transaction = new NestedTransaction()) {
- writeAssignedCertificate(certificate, transaction);
- transaction.commit();
- }
- }
-
- public void writeAssignedCertificate(AssignedCertificate certificate, NestedTransaction transaction) {
- Path path = endpointCertificatePath(certificate.application(), certificate.instance());
- curator.create(path);
- CuratorOperation operation = CuratorOperations.setData(path.getAbsolute(),
- asJson(EndpointCertificateSerializer.toSlime(certificate.certificate())));
- transaction.add(CuratorTransaction.from(operation, curator));
- }
-
- public void removeAssignedCertificate(TenantAndApplicationId application, Optional<InstanceName> instanceName) {
- curator.delete(endpointCertificatePath(application, instanceName));
- }
-
- public void removeAssignedCertificate(TenantAndApplicationId application, Optional<InstanceName> instanceName, NestedTransaction transaction) {
- transaction.add(CuratorTransaction.from(CuratorOperations.delete(endpointCertificatePath(application, instanceName).getAbsolute()), curator));
- }
-
- // TODO(mpolden): Remove this. Caller should make an explicit decision to read certificate for a particular instance
- public Optional<AssignedCertificate> readAssignedCertificate(ApplicationId applicationId) {
- return readAssignedCertificate(TenantAndApplicationId.from(applicationId), Optional.of(applicationId.instance()));
- }
-
- public Optional<AssignedCertificate> readAssignedCertificate(TenantAndApplicationId application, Optional<InstanceName> instance) {
- return readSlime(endpointCertificatePath(application, instance)).map(Slime::get)
- .map(EndpointCertificateSerializer::fromSlime)
- .map(cert -> new AssignedCertificate(application, instance, cert, false));
- }
-
- public List<AssignedCertificate> readAssignedCertificates() {
- List<AssignedCertificate> certificates = new ArrayList<>();
- for (String value : curator.getChildren(endpointCertificateRoot)) {
- final TenantAndApplicationId application;
- final Optional<InstanceName> instanceName;
- if (value.split(":").length == 3) {
- ApplicationId instance = ApplicationId.fromSerializedForm(value);
- application = TenantAndApplicationId.from(instance);
- instanceName = Optional.of(instance.instance());
- } else {
- application = TenantAndApplicationId.fromSerialized(value);
- instanceName = Optional.empty();
- }
- Optional<AssignedCertificate> assigned = readAssignedCertificate(application, instanceName);
- if (assigned.isEmpty()) continue; // Deleted while reading
- certificates.add(assigned.get());
- }
- return certificates;
- }
-
- // -------------- Metering view refresh times ----------------------------
-
- public void writeMeteringRefreshTime(long timestamp) {
- curator.set(meteringRefreshPath(), Long.toString(timestamp).getBytes());
- }
-
- public long readMeteringRefreshTime() {
- return curator.getData(meteringRefreshPath())
- .map(String::new).map(Long::parseLong)
- .orElse(0L);
- }
-
- // -------------- Archive buckets -----------------------------------------
-
- public ArchiveBuckets readArchiveBuckets(ZoneId zoneId) {
- return readSlime(archiveBucketsPath(zoneId)).map(ArchiveBucketsSerializer::fromSlime)
- .orElse(ArchiveBuckets.EMPTY);
- }
-
- public void writeArchiveBuckets(ZoneId zoneid, ArchiveBuckets archiveBuckets) {
- curator.set(archiveBucketsPath(zoneid), asJson(ArchiveBucketsSerializer.toSlime(archiveBuckets)));
- }
-
- // -------------- VCMRs ---------------------------------------------------
-
- public Optional<VespaChangeRequest> readChangeRequest(String changeRequestId) {
- return readSlime(changeRequestPath(changeRequestId)).map(ChangeRequestSerializer::fromSlime);
- }
-
- public List<VespaChangeRequest> readChangeRequests() {
- return curator.getChildren(changeRequestsRoot)
- .stream()
- .map(this::readChangeRequest)
- .flatMap(Optional::stream)
- .toList();
- }
-
- public void writeChangeRequest(VespaChangeRequest changeRequest) {
- curator.set(changeRequestPath(changeRequest.getId()), asJson(ChangeRequestSerializer.toSlime(changeRequest)));
- }
-
- public void deleteChangeRequest(VespaChangeRequest changeRequest) {
- curator.delete(changeRequestPath(changeRequest.getId()));
- }
-
- // -------------- Notifications -------------------------------------------
-
- public List<Notification> readNotifications(TenantName tenantName) {
- return readSlime(notificationsPath(tenantName))
- .map(slime -> notificationsSerializer.fromSlime(tenantName, slime)).orElseGet(List::of);
- }
-
-
- public List<TenantName> listTenantsWithNotifications() {
- return curator.getChildren(notificationsRoot).stream()
- .map(TenantName::from)
- .toList();
- }
-
- public void writeNotifications(TenantName tenantName, List<Notification> notifications) {
- curator.set(notificationsPath(tenantName), asJson(notificationsSerializer.toSlime(notifications)));
- }
-
- public void deleteNotifications(TenantName tenantName) {
- curator.delete(notificationsPath(tenantName));
- }
-
- // -------------- Endpoint Support Access ---------------------------------
-
- public SupportAccess readSupportAccess(DeploymentId deploymentId) {
- return readSlime(supportAccessPath(deploymentId)).map(SupportAccessSerializer::fromSlime).orElse(SupportAccess.DISALLOWED_NO_HISTORY);
- }
-
- public void writeSupportAccess(DeploymentId deploymentId, SupportAccess supportAccess) {
- curator.set(supportAccessPath(deploymentId), asJson(SupportAccessSerializer.toSlime(supportAccess)));
- }
-
- // -------------- Job Retrigger entries -----------------------------------
-
- public List<RetriggerEntry> readRetriggerEntries() {
- return readSlime(deploymentRetriggerPath()).map(retriggerEntrySerializer::fromSlime).orElseGet(List::of);
- }
-
- public void writeRetriggerEntries(List<RetriggerEntry> retriggerEntries) {
- curator.set(deploymentRetriggerPath(), asJson(retriggerEntrySerializer.toSlime(retriggerEntries)));
- }
-
- // -------------- Pending mail verification -------------------------------
-
- public Optional<PendingMailVerification> getPendingMailVerification(String verificationCode) {
- return readSlime(mailVerificationPath(verificationCode)).map(MailVerificationSerializer::fromSlime);
- }
-
- public List<PendingMailVerification> listPendingMailVerifications() {
- return curator.getChildren(mailVerificationRoot)
- .stream()
- .map(this::getPendingMailVerification)
- .flatMap(Optional::stream)
- .toList();
- }
-
- public void writePendingMailVerification(PendingMailVerification pendingMailVerification) {
- curator.set(mailVerificationPath(pendingMailVerification.getVerificationCode()), asJson(MailVerificationSerializer.toSlime(pendingMailVerification)));
- }
-
- public void deletePendingMailVerification(PendingMailVerification pendingMailVerification) {
- curator.delete(mailVerificationPath(pendingMailVerification.getVerificationCode()));
- }
-
- // -------------- Date plane tokens ---------------------------------------
-
- public void writeDataplaneTokens(TenantName tenantName, List<DataplaneTokenVersions> dataplaneTokenVersions) {
- curator.set(dataplaneTokenPath(tenantName), asJson(DataplaneTokenSerializer.toSlime(dataplaneTokenVersions)));
- }
-
- public List<DataplaneTokenVersions> readDataplaneTokens(TenantName tenantName) {
- return readSlime(dataplaneTokenPath(tenantName)).map(DataplaneTokenSerializer::fromSlime).orElse(List.of());
- }
-
- // -------------- Endpoint certificate pool -------------------------------
-
- public void writeUnassignedCertificate(UnassignedCertificate certificate) {
- curator.set(certificatePoolPath(certificate.id()), asJson(unassignedCertificateSerializer.toSlime(certificate)));
- }
-
- public Optional<UnassignedCertificate> readUnassignedCertificate(String id) {
- return readSlime(certificatePoolPath(id)).map(unassignedCertificateSerializer::fromSlime);
- }
-
- public void removeUnassignedCertificate(UnassignedCertificate certificate, NestedTransaction transaction) {
- Path path = certificatePoolPath(certificate.id());
- CuratorTransaction curatorTransaction = CuratorTransaction.from(CuratorOperations.delete(path.getAbsolute()), curator);
- transaction.add(curatorTransaction);
- }
-
- public List<UnassignedCertificate> readUnassignedCertificates() {
- return curator.getChildren(certificatePoolRoot).stream().flatMap(id -> readUnassignedCertificate(id).stream()).toList();
- }
-
- // -------------- Cloud trial notification --------------------------------
-
- public void writeTrialNotifications(TrialNotifications tn) {
- curator.set(trialNotificationsRoot, asJson(tn.toSlime()));
- }
-
- public Optional<TrialNotifications> readTrialNotifications() {
- return readSlime(trialNotificationsRoot).map(TrialNotifications::fromSlime);
- }
-
- // -------------- Paths ---------------------------------------------------
-
- private static Path upgradesPerMinutePath() {
- return root.append("upgrader").append("upgradesPerMinute");
- }
-
- private static Path confidenceOverridesPath() {
- return root.append("upgrader").append("confidenceOverrides");
- }
-
- private static Path osVersionTargetsPath() {
- return root.append("osUpgrader").append("targetVersion");
- }
-
- private static Path certifiedOsVersionsPath() {
- return root.append("osUpgrader").append("certifiedVersion");
- }
-
- private static Path osVersionStatusPath() {
- return root.append("osVersionStatus");
- }
-
- private static Path versionStatusPath() {
- return root.append("versionStatus");
- }
-
- private static Path routingPolicyPath(ApplicationId application) {
- return routingPoliciesRoot.append(application.serializedForm());
- }
-
- private static Path zoneRoutingPolicyPath(ZoneId zone) { return zoneRoutingPoliciesRoot.append(zone.value()); }
-
- private static Path nameServiceQueuePath() {
- return root.append("nameServiceQueue");
- }
-
- private static Path auditLogPath() {
- return root.append("auditLog");
- }
-
- private static Path provisionStatePath() {
- return root.append("provisioning").append("states");
- }
-
- private static Path provisionStatePath(String provisionId) {
- return provisionStatePath().append(provisionId);
- }
-
- private static Path tenantPath(TenantName name) {
- return tenantRoot.append(name.value());
- }
-
- private static Path applicationPath(TenantAndApplicationId id) {
- return applicationRoot.append(id.serialized());
- }
-
- private static Path runsPath(ApplicationId id, JobType type) {
- return jobRoot.append(id.serializedForm()).append(type.jobName());
- }
-
- private static Path lastRunPath(ApplicationId id, JobType type) {
- return runsPath(id, type).append("last");
- }
-
- private static Path logPath(ApplicationId id, JobType type, long first) {
- return runsPath(id, type).append("logs").append(Long.toString(first));
- }
-
- private static Path lastLogPath(ApplicationId id, JobType type) {
- return runsPath(id, type).append("logs");
- }
-
- private static Path controllerPath(String hostname) {
- return controllerRoot.append(hostname);
- }
-
- private static Path endpointCertificatePath(TenantAndApplicationId application, Optional<InstanceName> instance) {
- String id = instance.map(name -> application.instance(name).serializedForm())
- .orElseGet(application::serialized);
- return endpointCertificateRoot.append(id);
- }
-
- private static Path meteringRefreshPath() {
- return root.append("meteringRefreshTime");
- }
-
- private static Path archiveBucketsPath(ZoneId zoneId) {
- return archiveBucketsRoot.append(zoneId.value());
- }
-
- private static Path changeRequestPath(String id) {
- return changeRequestsRoot.append(id);
- }
-
- private static Path notificationsPath(TenantName tenantName) {
- return notificationsRoot.append(tenantName.value());
- }
-
- private static Path supportAccessPath(DeploymentId deploymentId) {
- return supportAccessRoot.append(deploymentId.dottedString());
- }
-
- private static Path deploymentRetriggerPath() {
- return root.append("deploymentRetriggerQueue");
- }
-
- private static Path mailVerificationPath(String verificationCode) {
- return mailVerificationRoot.append(verificationCode);
- }
-
- private static Path dataplaneTokenPath(TenantName tenantName) {
- return dataPlaneTokenRoot.append(tenantName.value());
- }
-
- private static Path certificatePoolPath(String id) {
- return certificatePoolRoot.append(id);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DataplaneTokenSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DataplaneTokenSerializer.java
deleted file mode 100644
index 6537bde467a..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DataplaneTokenSerializer.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.FingerPrint;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * @author mortent
- */
-public class DataplaneTokenSerializer {
-
- private static final String dataplaneTokenField = "dataplaneToken";
- private static final String idField = "id";
- private static final String tokenVersionsField = "tokenVersions";
- private static final String fingerPrintField = "fingerPrint";
- private static final String checkAccessHashField = "checkAccessHash";
- private static final String creationTimeField = "creationTime";
- private static final String authorField = "author";
- private static final String expirationField = "expiration";
- private static final String lastUpdatedField = "lastUpdated";
-
- public static Slime toSlime(List<DataplaneTokenVersions> dataplaneTokenVersions) {
- Slime slime = new Slime();
- Cursor cursor = slime.setObject();
- Cursor array = cursor.setArray(dataplaneTokenField);
- dataplaneTokenVersions.forEach(tokenMetadata -> {
- Cursor tokenCursor = array.addObject();
- tokenCursor.setString(idField, tokenMetadata.tokenId().value());
- tokenCursor.setLong(lastUpdatedField, tokenMetadata.lastUpdated().toEpochMilli());
- Cursor versionArray = tokenCursor.setArray(tokenVersionsField);
- tokenMetadata.tokenVersions().forEach(version -> {
- Cursor versionCursor = versionArray.addObject();
- versionCursor.setString(fingerPrintField, version.fingerPrint().value());
- versionCursor.setString(checkAccessHashField, version.checkAccessHash());
- versionCursor.setLong(creationTimeField, version.creationTime().toEpochMilli());
- versionCursor.setString(creationTimeField, version.creationTime().toString());
- versionCursor.setString(authorField, version.author());
- versionCursor.setString(expirationField, version.expiration().map(Instant::toString).orElse("<none>"));
- });
- });
- return slime;
- }
-
- public static List<DataplaneTokenVersions> fromSlime(Slime slime) {
- Cursor cursor = slime.get();
- return SlimeUtils.entriesStream(cursor.field(dataplaneTokenField))
- .map(entry -> {
- TokenId id = TokenId.of(entry.field(idField).asString());
- List<DataplaneTokenVersions.Version> versions = SlimeUtils.entriesStream(entry.field(tokenVersionsField))
- .map(versionCursor -> {
- FingerPrint fingerPrint = FingerPrint.of(versionCursor.field(fingerPrintField).asString());
- String checkAccessHash = versionCursor.field(checkAccessHashField).asString();
- Instant creationTime = SlimeUtils.instant(versionCursor.field(creationTimeField));
- String author = versionCursor.field(authorField).asString();
- String expirationStr = versionCursor.field(expirationField).asString();
- Optional<Instant> expiration = expirationStr.equals("<none>") ? Optional.empty()
- : (expirationStr.isBlank()
- ? Optional.of(Instant.EPOCH) : Optional.of(Instant.parse(expirationStr)));
- return new DataplaneTokenVersions.Version(fingerPrint, checkAccessHash, creationTime, expiration, author);
- })
- .toList();
- return new DataplaneTokenVersions(id, versions, Instant.ofEpochMilli(entry.field(lastUpdatedField).asLong()));
- })
- .toList();
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DnsChallengeSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DnsChallengeSerializer.java
deleted file mode 100644
index 4991d03d7df..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/DnsChallengeSerializer.java
+++ /dev/null
@@ -1,72 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService.DnsChallenge;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService.ChallengeState;
-
-import java.time.Instant;
-
-import static com.yahoo.yolean.Exceptions.uncheck;
-
-/**
- * @author jonmv
- */
-class DnsChallengeSerializer {
-
- private static final String nameField = "name";
- private static final String dataField = "data";
- private static final String serviceIdField = "serviceId";
- private static final String accountField = "account";
- private static final String createdAtField = "createdAt";
- private static final String stateField = "state";
-
- DnsChallenge fromJson(byte[] json, ClusterId clusterId) {
- Cursor object = SlimeUtils.jsonToSlime(json).get();
- return new DnsChallenge(RecordName.from(object.field(nameField).asString()),
- RecordData.from(object.field(dataField).asString()),
- clusterId,
- object.field(serviceIdField).asString(),
- SlimeUtils.optionalString(object.field(accountField)).map(CloudAccount::from),
- Instant.ofEpochMilli(object.field(createdAtField).asLong()),
- toState(object.field(stateField).asString()));
- }
-
- byte[] toJson(DnsChallenge challenge) {
- Slime slime = new Slime();
- Cursor object = slime.setObject();
- object.setString(nameField, challenge.name().name());
- object.setString(dataField, challenge.data().data());
- object.setString(serviceIdField, challenge.serviceId());
- challenge.account().ifPresent(account -> object.setString(accountField, account.value()));
- object.setLong(createdAtField, challenge.createdAt().toEpochMilli());
- object.setString(stateField, toString(challenge.state()));
- return uncheck(() -> SlimeUtils.toJsonBytes(slime));
- }
-
- private static ChallengeState toState(String value) {
- return switch (value) {
- case "pending" -> ChallengeState.pending;
- case "ready" -> ChallengeState.ready;
- case "running" -> ChallengeState.running;
- case "done" -> ChallengeState.done;
- default -> throw new IllegalArgumentException("invalid serialized state: " + value);
- };
- }
-
- private static String toString(ChallengeState state) {
- return switch (state) {
- case pending -> "pending";
- case ready -> "ready";
- case running -> "running";
- case done -> "done";
- };
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateSerializer.java
deleted file mode 100644
index b204e2fe328..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateSerializer.java
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.slime.Type;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-
-import java.util.Optional;
-import java.util.stream.IntStream;
-
-/**
- * Serializer for {@link EndpointCertificate}.
- *
- * @author andreer
- */
-public class EndpointCertificateSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster, and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private final static String keyNameField = "keyName";
- private final static String certNameField = "certName";
- private final static String versionField = "version";
- private final static String lastRequestedField = "lastRequested";
- private final static String rootRequestIdField = "requestId";
- private final static String leafRequestIdField = "leafRequestId";
- private final static String requestedDnsSansField = "requestedDnsSans";
- private final static String issuerField = "issuer";
- private final static String expiryField = "expiry";
- private final static String lastRefreshedField = "lastRefreshed";
- private final static String generatedIdField = "randomizedId";
-
- public static Slime toSlime(EndpointCertificate cert) {
- Slime slime = new Slime();
- Cursor object = slime.setObject();
- toSlime(cert, object);
- return slime;
- }
-
- public static void toSlime(EndpointCertificate cert, Cursor object) {
- object.setString(keyNameField, cert.keyName());
- object.setString(certNameField, cert.certName());
- object.setLong(versionField, cert.version());
- object.setLong(lastRequestedField, cert.lastRequested());
- object.setString(rootRequestIdField, cert.rootRequestId());
- cert.leafRequestId().ifPresent(leafRequestId -> object.setString(leafRequestIdField, leafRequestId));
- var cursor = object.setArray(requestedDnsSansField);
- cert.requestedDnsSans().forEach(cursor::addString);
- object.setString(issuerField, cert.issuer());
- cert.expiry().ifPresent(expiry -> object.setLong(expiryField, expiry));
- cert.lastRefreshed().ifPresent(refreshTime -> object.setLong(lastRefreshedField, refreshTime));
- cert.generatedId().ifPresent(id -> object.setString(generatedIdField, id));
- }
-
- public static EndpointCertificate fromSlime(Inspector inspector) {
- if (inspector.type() != Type.OBJECT)
- throw new IllegalArgumentException("Invalid format encountered for endpoint certificate");
-
- return new EndpointCertificate(
- inspector.field(keyNameField).asString(),
- inspector.field(certNameField).asString(),
- Math.toIntExact(inspector.field(versionField).asLong()),
- inspector.field(lastRequestedField).asLong(),
- inspector.field(rootRequestIdField).asString(),
- SlimeUtils.optionalString(inspector.field(leafRequestIdField)),
- IntStream.range(0, inspector.field(requestedDnsSansField).entries())
- .mapToObj(i -> inspector.field(requestedDnsSansField).entry(i).asString()).toList(),
- inspector.field(issuerField).asString(),
- inspector.field(expiryField).valid() ?
- Optional.of(inspector.field(expiryField).asLong()) :
- Optional.empty(),
- inspector.field(lastRefreshedField).valid() ?
- Optional.of(inspector.field(lastRefreshedField).asLong()) :
- Optional.empty(),
- inspector.field(generatedIdField).valid() ?
- Optional.of(inspector.field(generatedIdField).asString()) :
- Optional.empty());
- }
-
- public static EndpointCertificate fromJsonString(String zkData) {
- return fromSlime(SlimeUtils.jsonToSlime(zkData).get());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/JobControlFlags.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/JobControlFlags.java
deleted file mode 100644
index f699133ca53..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/JobControlFlags.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.concurrent.maintenance.JobControlState;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.ListFlag;
-import com.yahoo.vespa.flags.PermanentFlags;
-
-import java.util.Set;
-
-/**
- * An implementation of {@link JobControlState} that uses a feature flag to control maintenance jobs.
- *
- * @author mpolden
- */
-public class JobControlFlags implements JobControlState {
-
- private final CuratorDb curator;
- private final ListFlag<String> inactiveJobsFlag;
-
- public JobControlFlags(CuratorDb curator, FlagSource flagSource) {
- this.curator = curator;
- this.inactiveJobsFlag = PermanentFlags.INACTIVE_MAINTENANCE_JOBS.bindTo(flagSource);
- }
-
- @Override
- public Set<String> readInactiveJobs() {
- return Set.copyOf(inactiveJobsFlag.value());
- }
-
- @Override
- public Mutex lockMaintenanceJob(String job) {
- return curator.lockMaintenanceJob(job);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializer.java
deleted file mode 100644
index 69fe9bb8fa1..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializer.java
+++ /dev/null
@@ -1,120 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.ObjectTraverser;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * Serialisation of {@link LogEntry} objects. Not all fields are stored!
- *
- * @author jonmv
- */
-class LogSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String idField = "id";
- private static final String typeField = "type";
- private static final String timestampField = "at";
- private static final String messageField = "message";
-
- byte[] toJson(Map<Step, List<LogEntry>> log) {
- try {
- return SlimeUtils.toJsonBytes(toSlime(log));
- }
- catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- Slime toSlime(Map<Step, List<LogEntry>> log) {
- Slime root = new Slime();
- Cursor logObject = root.setObject();
- log.forEach((step, entries) -> {
- Cursor recordsArray = logObject.setArray(RunSerializer.valueOf(step));
- entries.forEach(entry -> toSlime(entry, recordsArray.addObject()));
- });
- return root;
- }
-
- private void toSlime(LogEntry entry, Cursor entryObject) {
- entryObject.setLong(idField, entry.id());
- entryObject.setLong(timestampField, entry.at().toEpochMilli());
- entryObject.setString(typeField, valueOf(entry.type()));
- entryObject.setString(messageField, entry.message());
- }
-
- Map<Step, List<LogEntry>> fromJson(byte[] logJson, long after) {
- return fromJson(Collections.singletonList(logJson), after);
- }
-
- Map<Step, List<LogEntry>> fromJson(List<byte[]> logJsons, long after) {
- return fromSlime(logJsons.stream()
- .map(SlimeUtils::jsonToSlime)
- .toList(),
- after);
- }
-
- Map<Step, List<LogEntry>> fromSlime(List<Slime> slimes, long after) {
- Map<Step, List<LogEntry>> log = new HashMap<>();
- slimes.forEach(slime -> slime.get().traverse((ObjectTraverser) (stepName, entryArray) -> {
- Step step = RunSerializer.stepOf(stepName);
- List<LogEntry> entries = log.computeIfAbsent(step, __ -> new ArrayList<>());
- entryArray.traverse((ArrayTraverser) (__, entryObject) -> {
- LogEntry entry = fromSlime(entryObject);
- if (entry.id() > after)
- entries.add(entry);
- });
- }));
- return log;
- }
-
- private LogEntry fromSlime(Inspector entryObject) {
- return new LogEntry(entryObject.field(idField).asLong(),
- SlimeUtils.instant(entryObject.field(timestampField)),
- typeOf(entryObject.field(typeField).asString()),
- entryObject.field(messageField).asString());
- }
-
- static String valueOf(Type type) {
- return switch (type) {
- case debug -> "debug";
- case info -> "info";
- case warning -> "warning";
- case error -> "error";
- case html -> "html";
- };
- }
-
- static Type typeOf(String type) {
- return switch (type) {
- case "debug" -> Type.debug;
- case "info" -> Type.info;
- case "warning" -> Type.warning;
- case "error" -> Type.error;
- case "html" -> Type.html;
- default -> throw new IllegalArgumentException("Unknown log entry type '" + type + "'!");
- };
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MailVerificationSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MailVerificationSerializer.java
deleted file mode 100644
index 44325853c15..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MailVerificationSerializer.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-
-import java.time.Instant;
-
-/**
- * @author olaa
- */
-public class MailVerificationSerializer {
-
- private static final String tenantField = "tenant";
- private static final String emailField = "email";
- private static final String emailTypeField = "emailType";
- private static final String emailVerificationCodeField = "emailVerificationCode";
- private static final String emailVerificationDeadlineField = "emailVerificationDeadline";
-
- public static Slime toSlime(PendingMailVerification pendingMailVerification) {
- var slime = new Slime();
- var object = slime.setObject();
- toSlime(pendingMailVerification, object);
- return slime;
- }
-
- public static void toSlime(PendingMailVerification pendingMailVerification, Cursor object) {
- object.setString(tenantField, pendingMailVerification.getTenantName().value());
- object.setString(emailVerificationCodeField, pendingMailVerification.getVerificationCode());
- object.setString(emailField, pendingMailVerification.getMailAddress());
- object.setLong(emailVerificationDeadlineField, pendingMailVerification.getVerificationDeadline().toEpochMilli());
- object.setString(emailTypeField, pendingMailVerification.getMailType().name());
- }
-
- public static PendingMailVerification fromSlime(Slime slime) {
- return fromSlime(slime.get());
- }
-
- public static PendingMailVerification fromSlime(Inspector inspector) {
- var tenant = TenantName.from(inspector.field(tenantField).asString());
- var address = inspector.field(emailField).asString();
- var verificationCode = inspector.field(emailVerificationCodeField).asString();
- var deadline = Instant.ofEpochMilli(inspector.field(emailVerificationDeadlineField).asLong());
- var type = PendingMailVerification.MailType.valueOf(inspector.field(emailTypeField).asString());
- return new PendingMailVerification(tenant, address, verificationCode, deadline, type);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MockCuratorDb.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MockCuratorDb.java
deleted file mode 100644
index 6ad77af08e2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/MockCuratorDb.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.cloud.config.ConfigserverConfig;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.curator.mock.MockCurator;
-
-import java.time.Duration;
-
-/**
- * A curator db backed by a mock curator.
- *
- * @author bratseth
- */
-@SuppressWarnings("unused") // injected
-public class MockCuratorDb extends CuratorDb {
-
- private final MockCurator curator;
-
- @Inject
- public MockCuratorDb(ConfigserverConfig config) {
- this("test-controller:2222");
- }
-
- public MockCuratorDb(SystemName system) {
- this("test-controller:2222");
- }
-
- public MockCuratorDb(String zooKeeperEnsembleConnectionSpec) {
- this(new MockCurator() { @Override public String zooKeeperEnsembleConnectionSpec() { return zooKeeperEnsembleConnectionSpec; } });
- }
-
- public MockCuratorDb(MockCurator curator) {
- super(curator, Duration.ofMillis(100));
- this.curator = curator;
- }
-
- public MockCurator curator() { return curator; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializer.java
deleted file mode 100644
index 4192f19298f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializer.java
+++ /dev/null
@@ -1,134 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.dns.CreateRecord;
-import com.yahoo.vespa.hosted.controller.dns.CreateRecords;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceRequest;
-import com.yahoo.vespa.hosted.controller.dns.RemoveRecords;
-
-import java.util.ArrayList;
-import java.util.Optional;
-
-/**
- * Serializer for {@link com.yahoo.vespa.hosted.controller.dns.NameServiceQueue}.
- *
- * @author mpolden
- */
-public class NameServiceQueueSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String requestsField = "requests";
- private static final String requestType = "requestType";
- private static final String recordsField = "records";
- private static final String typeField = "type";
- private static final String nameField = "name";
- private static final String dataField = "data";
- private static final String ownerField = "owner";
-
- public Slime toSlime(NameServiceQueue queue) {
- var slime = new Slime();
- var root = slime.setObject();
- var array = root.setArray(requestsField);
-
- for (var request : queue.requests()) {
- var object = array.addObject();
-
- request.owner().ifPresent(owner -> object.setString(ownerField, owner.serialized()));
-
- if (request instanceof CreateRecords) toSlime(object, (CreateRecords) request);
- else if (request instanceof CreateRecord) toSlime(object, (CreateRecord) request);
- else if (request instanceof RemoveRecords) toSlime(object, (RemoveRecords) request);
- else throw new IllegalArgumentException("No serialization defined for request of type " +
- request.getClass().getName());
- }
-
- return slime;
- }
-
- public NameServiceQueue fromSlime(Slime slime) {
- var items = new ArrayList<NameServiceRequest>();
- var root = slime.get();
- root.field(requestsField).traverse((ArrayTraverser) (i, object) -> {
- Optional<TenantAndApplicationId> owner = SlimeUtils.optionalString(object.field(ownerField)).map(TenantAndApplicationId::fromSerialized);
- var request = Request.valueOf(object.field(requestType).asString());
- switch (request) {
- case createRecords -> items.add(createRecordsFromSlime(object, owner));
- case createRecord -> items.add(createRecordFromSlime(object, owner));
- case removeRecords -> items.add(removeRecordsFromSlime(object, owner));
- default -> throw new IllegalArgumentException("No serialization defined for request " + request);
- }
- });
- return new NameServiceQueue(items);
- }
-
- private void toSlime(Cursor object, CreateRecord createRecord) {
- object.setString(requestType, Request.createRecord.name());
- toSlime(object, createRecord.record());
- }
-
- private void toSlime(Cursor object, CreateRecords createRecords) {
- object.setString(requestType, Request.createRecords.name());
- var recordArray = object.setArray(recordsField);
- createRecords.records().forEach(record -> toSlime(recordArray.addObject(), record));
- }
-
- private void toSlime(Cursor object, RemoveRecords removeRecords) {
- object.setString(requestType, Request.removeRecords.name());
- object.setString(typeField, removeRecords.type().name());
- object.setString(nameField, removeRecords.name().asString());
- removeRecords.data().ifPresent(data -> object.setString(dataField, data.asString()));
- }
-
- private void toSlime(Cursor object, Record record) {
- object.setString(typeField, record.type().name());
- object.setString(nameField, record.name().asString());
- object.setString(dataField, record.data().asString());
- }
-
- private CreateRecords createRecordsFromSlime(Inspector object, Optional<TenantAndApplicationId> owner) {
- var records = new ArrayList<Record>();
- object.field(recordsField).traverse((ArrayTraverser) (i, recordObject) -> records.add(recordFromSlime(recordObject)));
- return new CreateRecords(owner, records);
- }
-
- private CreateRecord createRecordFromSlime(Inspector object, Optional<TenantAndApplicationId> owner) {
- return new CreateRecord(owner, recordFromSlime(object));
- }
-
- private RemoveRecords removeRecordsFromSlime(Inspector object, Optional<TenantAndApplicationId> owner) {
- var type = Record.Type.valueOf(object.field(typeField).asString());
- var name = RecordName.from(object.field(nameField).asString());
- var data = SlimeUtils.optionalString(object.field(dataField)).map(RecordData::from);
- return new RemoveRecords(owner, type, name, data);
- }
-
- private Record recordFromSlime(Inspector object) {
- return new Record(Record.Type.valueOf(object.field(typeField).asString()),
- RecordName.from(object.field(nameField).asString()),
- RecordData.from(object.field(dataField).asString()));
- }
-
- private enum Request {
- createRecord,
- createRecords,
- removeRecords,
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java
deleted file mode 100644
index 1ac8aad74ba..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NodeVersionSerializer.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Serializer for {@link com.yahoo.vespa.hosted.controller.versions.NodeVersion}.
- *
- * @author mpolden
- */
-public class NodeVersionSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String hostnameField = "hostname";
- private static final String zoneField = "zone";
- private static final String wantedVersionField = "wantedVersion";
- private static final String suspendedAtField = "suspendedAt";
-
- public void nodeVersionsToSlime(List<NodeVersion> nodeVersions, Cursor array) {
- for (var nodeVersion : nodeVersions) {
- var nodeVersionObject = array.addObject();
- nodeVersionObject.setString(hostnameField, nodeVersion.hostname().value());
- nodeVersionObject.setString(zoneField, nodeVersion.zone().value());
- nodeVersionObject.setString(wantedVersionField, nodeVersion.wantedVersion().toFullString());
- nodeVersion.suspendedAt().ifPresent(suspendedAt -> nodeVersionObject.setLong(suspendedAtField,
- suspendedAt.toEpochMilli()));
- }
- }
-
- public List<NodeVersion> nodeVersionsFromSlime(Inspector array, Version version) {
- List<NodeVersion> nodeVersions = new ArrayList<>();
- array.traverse((ArrayTraverser) (i, entry) -> {
- var hostname = HostName.of(entry.field(hostnameField).asString());
- var zone = ZoneId.from(entry.field(zoneField).asString());
- var wantedVersion = Version.fromString(entry.field(wantedVersionField).asString());
- var suspendedAt = SlimeUtils.optionalInstant(entry.field(suspendedAtField));
- nodeVersions.add(new NodeVersion(hostname, zone, version, wantedVersion, suspendedAt));
- });
- return nodeVersions;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
deleted file mode 100644
index d5be4d22dc2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializer.java
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.ObjectTraverser;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.notification.MailTemplating;
-import com.yahoo.vespa.hosted.controller.notification.Notification;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-
-import java.util.List;
-import java.util.Optional;
-
-/**
- * (de)serializes notifications for a tenant
- *
- * @author freva
- */
-public class NotificationsSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String notificationsFieldName = "notifications";
- private static final String atFieldName = "at";
- private static final String typeField = "type";
- private static final String levelField = "level";
- private static final String titleField = "title";
- private static final String messagesField = "messages";
- private static final String applicationField = "application";
- private static final String instanceField = "instance";
- private static final String zoneField = "zone";
- private static final String clusterIdField = "clusterId";
- private static final String jobTypeField = "jobId";
- private static final String runNumberField = "runNumber";
-
- public Slime toSlime(List<Notification> notifications) {
- Slime slime = new Slime();
- Cursor notificationsArray = slime.setObject().setArray(notificationsFieldName);
-
- for (Notification notification : notifications) {
- Cursor notificationObject = notificationsArray.addObject();
- notificationObject.setLong(atFieldName, notification.at().toEpochMilli());
- notificationObject.setString(typeField, asString(notification.type()));
- notificationObject.setString(levelField, asString(notification.level()));
- notificationObject.setString(titleField, notification.title());
- Cursor messagesArray = notificationObject.setArray(messagesField);
- notification.messages().forEach(messagesArray::addString);
-
- notification.source().application().ifPresent(application -> notificationObject.setString(applicationField, application.value()));
- notification.source().instance().ifPresent(instance -> notificationObject.setString(instanceField, instance.value()));
- notification.source().zoneId().ifPresent(zoneId -> notificationObject.setString(zoneField, zoneId.value()));
- notification.source().clusterId().ifPresent(clusterId -> notificationObject.setString(clusterIdField, clusterId.value()));
- notification.source().jobType().ifPresent(jobType -> notificationObject.setString(jobTypeField, jobType.serialized()));
- notification.source().runNumber().ifPresent(runNumber -> notificationObject.setLong(runNumberField, runNumber));
-
- notification.mailContent().ifPresent(mc -> {
- notificationObject.setString("mail-template", mc.template().getId());
- mc.subject().ifPresent(s -> notificationObject.setString("mail-subject", s));
- var mailParamsCursor = notificationObject.setObject("mail-params");
- mc.values().forEach((key, value) -> {
- if (value instanceof String str) {
- mailParamsCursor.setString(key, str);
- } else if (value instanceof List<?> l) {
- var array = mailParamsCursor.setArray(key);
- l.forEach(elem -> array.addString((String) elem));
- } else {
- throw new ClassCastException("Unsupported param type: " + value.getClass());
- }
- });
- });
- }
-
- return slime;
- }
-
- public List<Notification> fromSlime(TenantName tenantName, Slime slime) {
- return SlimeUtils.entriesStream(slime.get().field(notificationsFieldName))
- .filter(inspector -> { // TODO: remove in summer.
- if (!inspector.field(jobTypeField).valid()) return true;
- try {
- JobType.ofSerialized(inspector.field(jobTypeField).asString());
- return true;
- } catch (RuntimeException e) {
- return false;
- }
- })
- .map(inspector -> fromInspector(tenantName, inspector)).toList();
- }
-
- private Notification fromInspector(TenantName tenantName, Inspector inspector) {
- return new Notification(
- SlimeUtils.instant(inspector.field(atFieldName)),
- typeFrom(inspector.field(typeField)),
- levelFrom(inspector.field(levelField)),
- new NotificationSource(
- tenantName,
- SlimeUtils.optionalString(inspector.field(applicationField)).map(ApplicationName::from),
- SlimeUtils.optionalString(inspector.field(instanceField)).map(InstanceName::from),
- SlimeUtils.optionalString(inspector.field(zoneField)).map(ZoneId::from),
- SlimeUtils.optionalString(inspector.field(clusterIdField)).map(ClusterSpec.Id::from),
- SlimeUtils.optionalString(inspector.field(jobTypeField)).map(jobName -> JobType.ofSerialized(jobName)),
- SlimeUtils.optionalLong(inspector.field(runNumberField))),
- SlimeUtils.optionalString(inspector.field(titleField)).orElse(""),
- SlimeUtils.entriesStream(inspector.field(messagesField)).map(Inspector::asString).toList(),
- mailContentFrom(inspector));
- }
-
- private Optional<Notification.MailContent> mailContentFrom(final Inspector inspector) {
- return SlimeUtils.optionalString(inspector.field("mail-template")).map(template -> {
- var builder = Notification.MailContent.fromTemplate(MailTemplating.Template.fromId(template).orElseThrow());
- SlimeUtils.optionalString(inspector.field("mail-subject")).ifPresent(builder::subject);
- inspector.field("mail-params").traverse((ObjectTraverser) (name, insp) -> {
- switch (insp.type()) {
- case STRING -> builder.with(name, insp.asString());
- case ARRAY -> builder.with(name, SlimeUtils.entriesStream(insp).map(Inspector::asString).toList());
- default -> throw new IllegalArgumentException("Unsupported param type: " + insp.type());
- }
- });
- return builder.build();
- });
- }
-
- private static String asString(Notification.Type type) {
- return switch (type) {
- case applicationPackage -> "applicationPackage";
- case submission -> "submission";
- case testPackage -> "testPackage";
- case deployment -> "deployment";
- case feedBlock -> "feedBlock";
- case reindex -> "reindex";
- case account -> "account";
- };
- }
-
- private static Notification.Type typeFrom(Inspector field) {
- return switch (field.asString()) {
- case "applicationPackage" -> Notification.Type.applicationPackage;
- case "submission" -> Notification.Type.submission;
- case "testPackage" -> Notification.Type.testPackage;
- case "deployment" -> Notification.Type.deployment;
- case "feedBlock" -> Notification.Type.feedBlock;
- case "reindex" -> Notification.Type.reindex;
- case "account" -> Notification.Type.account;
- default -> throw new IllegalArgumentException("Unknown serialized notification type value '" + field.asString() + "'");
- };
- }
-
- private static String asString(Notification.Level level) {
- return switch (level) {
- case info -> "info";
- case warning -> "warning";
- case error -> "error";
- };
- }
-
- private static Notification.Level levelFrom(Inspector field) {
- return switch (field.asString()) {
- case "info" -> Notification.Level.info;
- case "warning" -> Notification.Level.warning;
- case "error" -> Notification.Level.error;
- default -> throw new IllegalArgumentException("Unknown serialized notification level value '" + field.asString() + "'");
- };
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionSerializer.java
deleted file mode 100644
index 173ebf151aa..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionSerializer.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-
-import java.util.Collections;
-import java.util.Set;
-import java.util.TreeSet;
-
-/**
- * Serializer for an {@link OsVersion}.
- *
- * @author mpolden
- */
-public class OsVersionSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String versionsField = "versions";
- private static final String versionField = "version";
- private static final String cloudField = "cloud";
-
- public Slime toSlime(Set<OsVersion> osVersions) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor array = root.setArray(versionsField);
- osVersions.forEach(osVersion -> toSlime(osVersion, array.addObject()));
- return slime;
- }
-
- public void toSlime(OsVersion osVersion, Cursor object) {
- object.setString(versionField, osVersion.version().toFullString());
- object.setString(cloudField, osVersion.cloud().value());
- }
-
- public Set<OsVersion> fromSlime(Slime slime) {
- Inspector array = slime.get().field(versionsField);
- Set<OsVersion> osVersions = new TreeSet<>();
- array.traverse((ArrayTraverser) (i, inspector) -> osVersions.add(fromSlime(inspector)));
- return Collections.unmodifiableSet(osVersions);
- }
-
- public OsVersion fromSlime(Inspector object) {
- return new OsVersion(
- Version.fromString(object.field(versionField).asString()),
- CloudName.from(object.field(cloudField).asString())
- );
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializer.java
deleted file mode 100644
index 40826079efd..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializer.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSortedMap;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
-
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Serializer for {@link OsVersionStatus}.
- *
- * @author mpolden
- */
-public class OsVersionStatusSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String versionsField = "versions";
- private static final String nodeVersionsField = "nodeVersions";
-
- private final OsVersionSerializer osVersionSerializer;
- private final NodeVersionSerializer nodeVersionSerializer;
-
- public OsVersionStatusSerializer(OsVersionSerializer osVersionSerializer, NodeVersionSerializer nodeVersionSerializer) {
- this.osVersionSerializer = Objects.requireNonNull(osVersionSerializer, "osVersionSerializer must be non-null");
- this.nodeVersionSerializer = Objects.requireNonNull(nodeVersionSerializer, "nodeVersionSerializer must be non-null");
- }
-
- public Slime toSlime(OsVersionStatus status) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor versions = root.setArray(versionsField);
- status.versions().forEach((version, nodes) -> {
- Cursor object = versions.addObject();
- osVersionSerializer.toSlime(version, object);
- nodeVersionSerializer.nodeVersionsToSlime(nodes, object.setArray(nodeVersionsField));
- });
- return slime;
- }
-
- public OsVersionStatus fromSlime(Slime slime) {
- return new OsVersionStatus(osVersionsFromSlime(slime.get().field(versionsField)));
- }
-
- private ImmutableMap<OsVersion, List<NodeVersion>> osVersionsFromSlime(Inspector array) {
- var versions = ImmutableSortedMap.<OsVersion, List<NodeVersion>>naturalOrder();
- array.traverse((ArrayTraverser) (i, object) -> {
- OsVersion osVersion = osVersionSerializer.fromSlime(object);
- versions.put(osVersion, nodeVersionSerializer.nodeVersionsFromSlime(object.field(nodeVersionsField), osVersion.version()));
- });
- return versions.build();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionTargetSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionTargetSerializer.java
deleted file mode 100644
index 968cea33162..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionTargetSerializer.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
-
-import java.time.Instant;
-import java.util.Collections;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-/**
- * Serializer for {@link com.yahoo.vespa.hosted.controller.versions.OsVersionTarget}.
- *
- * @author mpolden
- */
-public class OsVersionTargetSerializer {
-
- private final OsVersionSerializer osVersionSerializer;
-
- private static final String versionsField = "versions";
- private static final String scheduledAtField = "scheduledAt";
- private static final String pinnedField = "pinned";
- private static final String downgradeField = "downgrade";
-
- public OsVersionTargetSerializer(OsVersionSerializer osVersionSerializer) {
- this.osVersionSerializer = osVersionSerializer;
- }
-
- public Slime toSlime(SortedSet<OsVersionTarget> osVersionTargets) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor array = root.setArray(versionsField);
- osVersionTargets.forEach(target -> toSlime(target, array.addObject()));
- return slime;
- }
-
- public Set<OsVersionTarget> fromSlime(Slime slime) {
- Inspector array = slime.get().field(versionsField);
- Set<OsVersionTarget> osVersionTargets = new TreeSet<>();
- array.traverse((ArrayTraverser) (i, inspector) -> {
- OsVersion osVersion = osVersionSerializer.fromSlime(inspector);
- Instant scheduledAt = SlimeUtils.instant(inspector.field(scheduledAtField));
- boolean pinned = inspector.field(pinnedField).asBool();
- boolean downgrade = inspector.field(downgradeField).asBool();
- osVersionTargets.add(new OsVersionTarget(osVersion, scheduledAt, pinned, downgrade));
- });
- return Collections.unmodifiableSet(osVersionTargets);
- }
-
- private void toSlime(OsVersionTarget target, Cursor object) {
- osVersionSerializer.toSlime(target.osVersion(), object);
- object.setLong(scheduledAtField, target.scheduledAt().toEpochMilli());
- object.setBool(pinnedField, target.pinned());
- object.setBool(downgradeField, target.downgrade());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializer.java
deleted file mode 100644
index 5e3f6675955..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializer.java
+++ /dev/null
@@ -1,157 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import ai.vespa.http.DomainName;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.GeneratedEndpoint;
-import com.yahoo.vespa.hosted.controller.routing.GeneratedEndpointList;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicy;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicyId;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Serializer and deserializer for a {@link RoutingPolicy}.
- *
- * @author mortent
- * @author mpolden
- */
-public class RoutingPolicySerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String routingPoliciesField = "routingPolicies";
- private static final String clusterField = "cluster";
- private static final String canonicalNameField = "canonicalName";
- private static final String ipAddressField = "ipAddress";
- private static final String zoneField = "zone";
- private static final String dnsZoneField = "dnsZone";
- private static final String instanceEndpointsField = "rotations";
- private static final String applicationEndpointsField = "applicationEndpoints";
- private static final String globalRoutingField = "globalRouting";
- private static final String agentField = "agent";
- private static final String changedAtField = "changedAt";
- private static final String statusField = "status";
- private static final String privateOnlyField = "private";
- private static final String generatedEndpointsField = "generatedEndpoints";
- private static final String clusterPartField = "clusterPart";
- private static final String applicationPartField = "applicationPart";
- private static final String authMethodField = "authMethod";
- private static final String endpointIdField = "endpointId";
-
- public Slime toSlime(List<RoutingPolicy> routingPolicies) {
- var slime = new Slime();
- var root = slime.setObject();
- var policyArray = root.setArray(routingPoliciesField);
- routingPolicies.forEach(policy -> {
- var policyObject = policyArray.addObject();
- policyObject.setString(clusterField, policy.id().cluster().value());
- policyObject.setString(zoneField, policy.id().zone().value());
- policy.canonicalName().map(DomainName::value).ifPresent(name -> policyObject.setString(canonicalNameField, name));
- policy.ipAddress().ifPresent(ipAddress -> policyObject.setString(ipAddressField, ipAddress));
- policy.dnsZone().ifPresent(dnsZone -> policyObject.setString(dnsZoneField, dnsZone));
- var instanceEndpointsArray = policyObject.setArray(instanceEndpointsField);
- policy.instanceEndpoints().forEach(endpointId -> instanceEndpointsArray.addString(endpointId.id()));
- var applicationEndpointsArray = policyObject.setArray(applicationEndpointsField);
- policy.applicationEndpoints().forEach(endpointId -> applicationEndpointsArray.addString(endpointId.id()));
- globalRoutingToSlime(policy.routingStatus(), policyObject.setObject(globalRoutingField));
- if ( ! policy.isPublic()) policyObject.setBool(privateOnlyField, true);
- Cursor generatedEndpointsArray = policyObject.setArray(generatedEndpointsField);
- policy.generatedEndpoints().forEach(generatedEndpoint -> {
- Cursor generatedEndpointObject = generatedEndpointsArray.addObject();
- generatedEndpointObject.setString(clusterPartField, generatedEndpoint.clusterPart());
- generatedEndpointObject.setString(applicationPartField, generatedEndpoint.applicationPart());
- generatedEndpointObject.setString(authMethodField, authMethod(generatedEndpoint.authMethod()));
- generatedEndpoint.endpoint().ifPresent(endpointId -> generatedEndpointObject.setString(endpointIdField, endpointId.id()));
- });
- });
- return slime;
- }
-
- public List<RoutingPolicy> fromSlime(ApplicationId owner, Slime slime) {
- List<RoutingPolicy> policies = new ArrayList<>();
- var root = slime.get();
- var field = root.field(routingPoliciesField);
- field.traverse((ArrayTraverser) (i, inspect) -> {
- Set<EndpointId> instanceEndpoints = new LinkedHashSet<>();
- inspect.field(instanceEndpointsField).traverse((ArrayTraverser) (j, endpointId) -> instanceEndpoints.add(EndpointId.of(endpointId.asString())));
- Set<EndpointId> applicationEndpoints = new LinkedHashSet<>();
- inspect.field(applicationEndpointsField).traverse((ArrayTraverser) (idx, endpointId) -> applicationEndpoints.add(EndpointId.of(endpointId.asString())));
- RoutingPolicyId id = new RoutingPolicyId(owner,
- ClusterSpec.Id.from(inspect.field(clusterField).asString()),
- ZoneId.from(inspect.field(zoneField).asString()));
- boolean isPublic = ! inspect.field(privateOnlyField).asBool();
- List<GeneratedEndpoint> generatedEndpoints = new ArrayList<>();
- Inspector generatedEndpointsArray = inspect.field(generatedEndpointsField);
- if (generatedEndpointsArray.valid()) {
- generatedEndpointsArray.traverse((ArrayTraverser) (idx, generatedEndpointObject) ->
- generatedEndpoints.add(new GeneratedEndpoint(generatedEndpointObject.field(clusterPartField).asString(),
- generatedEndpointObject.field(applicationPartField).asString(),
- authMethodFromSlime(generatedEndpointObject.field(authMethodField)),
- SlimeUtils.optionalString(generatedEndpointObject.field(endpointIdField))
- .map(EndpointId::of))));
- }
- policies.add(new RoutingPolicy(id,
- SlimeUtils.optionalString(inspect.field(canonicalNameField)).map(DomainName::of),
- SlimeUtils.optionalString(inspect.field(ipAddressField)),
- SlimeUtils.optionalString(inspect.field(dnsZoneField)),
- instanceEndpoints,
- applicationEndpoints,
- routingStatusFromSlime(inspect.field(globalRoutingField)),
- isPublic,
- GeneratedEndpointList.copyOf(generatedEndpoints)));
- });
- return Collections.unmodifiableList(policies);
- }
-
- public void globalRoutingToSlime(RoutingStatus routingStatus, Cursor object) {
- object.setString(statusField, routingStatus.value().name());
- object.setString(agentField, routingStatus.agent().name());
- object.setLong(changedAtField, routingStatus.changedAt().toEpochMilli());
- }
-
- public RoutingStatus routingStatusFromSlime(Inspector object) {
- var status = RoutingStatus.Value.valueOf(object.field(statusField).asString());
- var agent = RoutingStatus.Agent.valueOf(object.field(agentField).asString());
- var changedAt = SlimeUtils.optionalInstant(object.field(changedAtField)).orElse(Instant.EPOCH);
- return new RoutingStatus(status, agent, changedAt);
- }
-
- private String authMethod(AuthMethod authMethod) {
- return switch (authMethod) {
- case token -> "token";
- case mtls -> "mtls";
- case none -> "none";
- };
- }
-
- private AuthMethod authMethodFromSlime(Inspector field) {
- return switch (field.asString()) {
- case "token" -> AuthMethod.token;
- case "mtls" -> AuthMethod.mtls;
- case "none" -> AuthMethod.none;
- default -> throw new IllegalArgumentException("Unknown auth method '" + field.asString() + "'");
- };
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
deleted file mode 100644
index 1d28432039b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializer.java
+++ /dev/null
@@ -1,418 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.ObjectTraverser;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.deployment.ConvergenceSummary;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.Run.Reason;
-import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-import com.yahoo.vespa.hosted.controller.deployment.Step.Status;
-import com.yahoo.vespa.hosted.controller.deployment.StepInfo;
-import com.yahoo.vespa.hosted.controller.deployment.Versions;
-
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.NavigableMap;
-import java.util.Optional;
-import java.util.TreeMap;
-
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.aborted;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.cancelled;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.deploymentFailed;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.endpointCertificateTimeout;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.error;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.invalidApplication;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.noTests;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.nodeAllocationFailure;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.quotaExceeded;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.reset;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.success;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.testFailure;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.failed;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.copyVespaLogs;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployInitialReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.endStagingSetup;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.endTests;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installInitialReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.report;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.startStagingSetup;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.startTests;
-import static java.util.Comparator.comparing;
-
-/**
- * Serialises and deserialises {@link Run} objects for persistent storage.
- *
- * @author jonmv
- */
-class RunSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String stepsField = "steps";
- private static final String stepDetailsField = "stepDetails";
- private static final String startTimeField = "startTime";
- private static final String applicationField = "id";
- private static final String jobTypeField = "type";
- private static final String numberField = "number";
- private static final String startField = "start";
- private static final String endField = "end";
- private static final String sleepingUntilField = "sleepingUntil";
- private static final String statusField = "status";
- private static final String versionsField = "versions";
- private static final String isRedeploymentField = "isRedeployment";
- private static final String platformVersionField = "platform";
- private static final String deployedDirectlyField = "deployedDirectly";
- private static final String buildField = "build";
- private static final String sourceField = "source";
- private static final String lastTestRecordField = "lastTestRecord";
- private static final String lastVespaLogTimestampField = "lastVespaLogTimestamp";
- private static final String noNodesDownSinceField = "noNodesDownSince";
- private static final String convergenceSummaryField = "convergenceSummaryV2";
- private static final String testerCertificateField = "testerCertificate";
- private static final String isDryRunField = "isDryRun";
- private static final String cloudAccountField = "account";
- private static final String reasonField = "reason";
- private static final String dependentField = "dependent";
- private static final String changeField = "change";
-
- Run runFromSlime(Slime slime) {
- return runFromSlime(slime.get());
- }
-
- NavigableMap<RunId, Run> runsFromSlime(Slime slime) {
- NavigableMap<RunId, Run> runs = new TreeMap<>(comparing(RunId::number));
- Inspector runArray = slime.get();
- runArray.traverse((ArrayTraverser) (__, runObject) -> {
- Run run = runFromSlime(runObject);
- runs.put(run.id(), run);
- });
- return Collections.unmodifiableNavigableMap(runs);
- }
-
- private Run runFromSlime(Inspector runObject) {
- var steps = new EnumMap<Step, StepInfo>(Step.class);
- Inspector detailsField = runObject.field(stepDetailsField);
- runObject.field(stepsField).traverse((ObjectTraverser) (step, status) -> {
- Step typedStep = stepOf(step);
-
- // For historical reasons are the step details stored in a separate JSON structure from the step statuses.
- Inspector stepDetailsField = detailsField.field(step);
- Inspector startTimeValue = stepDetailsField.field(startTimeField);
- Optional<Instant> startTime = SlimeUtils.optionalInstant(startTimeValue);
-
- steps.put(typedStep, new StepInfo(typedStep, stepStatusOf(status.asString()), startTime));
- });
- RunId id = new RunId(ApplicationId.fromSerializedForm(runObject.field(applicationField).asString()),
- JobType.ofSerialized(runObject.field(jobTypeField).asString()),
- runObject.field(numberField).asLong());
- return new Run(id,
- steps,
- versionsFromSlime(runObject.field(versionsField), id),
- runObject.field(isRedeploymentField).asBool(),
- SlimeUtils.instant(runObject.field(startField)),
- SlimeUtils.optionalInstant(runObject.field(endField)),
- SlimeUtils.optionalInstant(runObject.field(sleepingUntilField)),
- runStatusOf(runObject.field(statusField).asString()),
- runObject.field(lastTestRecordField).asLong(),
- Instant.EPOCH.plus(runObject.field(lastVespaLogTimestampField).asLong(), ChronoUnit.MICROS),
- SlimeUtils.optionalInstant(runObject.field(noNodesDownSinceField)),
- convergenceSummaryFrom(runObject.field(convergenceSummaryField)),
- SlimeUtils.optionalString(runObject.field(testerCertificateField)).map(X509CertificateUtils::fromPem),
- runObject.field(isDryRunField).valid() && runObject.field(isDryRunField).asBool(),
- SlimeUtils.optionalString(runObject.field(cloudAccountField)).map(CloudAccount::from),
- reasonFrom(runObject));
- }
-
- private Versions versionsFromSlime(Inspector versionsObject, RunId id) {
- Version targetPlatformVersion = Version.fromString(versionsObject.field(platformVersionField).asString());
- RevisionId targetRevision = revisionFrom(versionsObject, id);
-
- Optional<Version> sourcePlatformVersion = versionsObject.field(sourceField).valid()
- ? Optional.of(Version.fromString(versionsObject.field(sourceField).field(platformVersionField).asString()))
- : Optional.empty();
- Optional<RevisionId> sourceRevision = versionsObject.field(sourceField).valid()
- ? Optional.of(revisionFrom(versionsObject.field(sourceField), id))
- : Optional.empty();
-
- return new Versions(targetPlatformVersion, targetRevision, sourcePlatformVersion, sourceRevision);
- }
-
- private RevisionId revisionFrom(Inspector versionObject, RunId id) {
- long buildNumber = versionObject.field(buildField).asLong();
- boolean production = versionObject.field(deployedDirectlyField).valid() // TODO jonmv: remove after migration
- && buildNumber > 0
- && ! versionObject.field(deployedDirectlyField).asBool();
- return production ? RevisionId.forProduction(buildNumber) : RevisionId.forDevelopment(buildNumber, id.job());
- }
-
- // Don't change this — introduce a separate array instead.
- private Optional<ConvergenceSummary> convergenceSummaryFrom(Inspector summaryArray) {
- if ( ! summaryArray.valid()) return Optional.empty();
-
- if (summaryArray.entries() != 12 && summaryArray.entries() != 13)
- throw new IllegalArgumentException("Convergence summary must have 13 entries");
-
- return Optional.of(new ConvergenceSummary(summaryArray.entry(0).asLong(),
- summaryArray.entry(1).asLong(),
- summaryArray.entry(2).asLong(),
- summaryArray.entry(3).asLong(),
- summaryArray.entry(4).asLong(),
- summaryArray.entry(5).asLong(),
- summaryArray.entry(6).asLong(),
- summaryArray.entry(7).asLong(),
- summaryArray.entry(8).asLong(),
- summaryArray.entry(9).asLong(),
- summaryArray.entry(10).asLong(),
- summaryArray.entry(11).asLong(),
- summaryArray.entry(12).asLong()));
- }
-
- Slime toSlime(Iterable<Run> runs) {
- Slime slime = new Slime();
- Cursor runArray = slime.setArray();
- runs.forEach(run -> toSlime(run, runArray.addObject()));
- return slime;
- }
-
- Slime toSlime(Run run) {
- Slime slime = new Slime();
- toSlime(run, slime.setObject());
- return slime;
- }
-
- private void toSlime(Run run, Cursor runObject) {
- runObject.setString(applicationField, run.id().application().serializedForm());
- runObject.setString(jobTypeField, run.id().type().serialized());
- runObject.setBool(isRedeploymentField, run.isRedeployment());
- runObject.setLong(numberField, run.id().number());
- runObject.setLong(startField, run.start().toEpochMilli());
- run.end().ifPresent(end -> runObject.setLong(endField, end.toEpochMilli()));
- run.sleepUntil().ifPresent(end -> runObject.setLong(sleepingUntilField, end.toEpochMilli()));
- runObject.setString(statusField, valueOf(run.status()));
- runObject.setLong(lastTestRecordField, run.lastTestLogEntry());
- if (run.lastVespaLogTimestamp().isAfter(Instant.EPOCH)) runObject.setLong(lastVespaLogTimestampField, Instant.EPOCH.until(run.lastVespaLogTimestamp(), ChronoUnit.MICROS));
- run.noNodesDownSince().ifPresent(noNodesDownSince -> runObject.setLong(noNodesDownSinceField, noNodesDownSince.toEpochMilli()));
- run.convergenceSummary().ifPresent(convergenceSummary -> toSlime(convergenceSummary, runObject.setArray(convergenceSummaryField)));
- run.testerCertificate().ifPresent(certificate -> runObject.setString(testerCertificateField, X509CertificateUtils.toPem(certificate)));
-
- Cursor stepsObject = runObject.setObject(stepsField);
- run.steps().forEach((step, statusInfo) -> stepsObject.setString(valueOf(step), valueOf(statusInfo.status())));
-
- // For historical reasons are the step details stored in a different field from the step statuses.
- Cursor stepDetailsObject = runObject.setObject(stepDetailsField);
- run.steps().forEach((step, statusInfo) ->
- statusInfo.startTime().ifPresent(startTime ->
- stepDetailsObject.setObject(valueOf(step)).setLong(startTimeField, valueOf(startTime))));
-
- Cursor versionsObject = runObject.setObject(versionsField);
- toSlime(run.versions().targetPlatform(), run.versions().targetRevision(), versionsObject);
- run.versions().sourcePlatform().ifPresent(sourcePlatformVersion -> {
- toSlime(sourcePlatformVersion,
- run.versions().sourceRevision()
- .orElseThrow(() -> new IllegalArgumentException("Source versions must be both present or absent.")),
- versionsObject.setObject(sourceField));
- });
- runObject.setBool(isDryRunField, run.isDryRun());
- run.cloudAccount().ifPresent(account -> runObject.setString(cloudAccountField, account.value()));
- toSlime(run.reason(), runObject);
- }
-
- private void toSlime(Version platformVersion, RevisionId revsion, Cursor versionsObject) {
- versionsObject.setString(platformVersionField, platformVersion.toString());
- versionsObject.setLong(buildField, revsion.number());
- versionsObject.setBool(deployedDirectlyField, ! revsion.isProduction());
- }
-
- // Don't change this - introduce a separate array with new values if needed.
- private void toSlime(ConvergenceSummary summary, Cursor summaryArray) {
- summaryArray.addLong(summary.nodes());
- summaryArray.addLong(summary.down());
- summaryArray.addLong(summary.upgradingOs());
- summaryArray.addLong(summary.upgradingFirmware());
- summaryArray.addLong(summary.needPlatformUpgrade());
- summaryArray.addLong(summary.upgradingPlatform());
- summaryArray.addLong(summary.needReboot());
- summaryArray.addLong(summary.rebooting());
- summaryArray.addLong(summary.needRestart());
- summaryArray.addLong(summary.restarting());
- summaryArray.addLong(summary.services());
- summaryArray.addLong(summary.needNewConfig());
- summaryArray.addLong(summary.retiring());
- }
-
- static String valueOf(Step step) {
- switch (step) {
- case deployInitialReal : return "deployInitialReal";
- case installInitialReal : return "installInitialReal";
- case deployReal : return "deployReal";
- case installReal : return "installReal";
- case deactivateReal : return "deactivateReal";
- case deployTester : return "deployTester";
- case installTester : return "installTester";
- case deactivateTester : return "deactivateTester";
- case copyVespaLogs : return "copyVespaLogs";
- case startStagingSetup : return "startStagingSetup";
- case endStagingSetup : return "endStagingSetup";
- case startTests : return "startTests";
- case endTests : return "endTests";
- case report : return "report";
-
- default: throw new AssertionError("No value defined for '" + step + "'!");
- }
- }
-
- static Step stepOf(String step) {
- switch (step) {
- case "deployInitialReal" : return deployInitialReal;
- case "installInitialReal" : return installInitialReal;
- case "deployReal" : return deployReal;
- case "installReal" : return installReal;
- case "deactivateReal" : return deactivateReal;
- case "deployTester" : return deployTester;
- case "installTester" : return installTester;
- case "deactivateTester" : return deactivateTester;
- case "copyVespaLogs" : return copyVespaLogs;
- case "startStagingSetup" : return startStagingSetup;
- case "endStagingSetup" : return endStagingSetup;
- case "startTests" : return startTests;
- case "endTests" : return endTests;
- case "report" : return report;
-
- default: throw new IllegalArgumentException("No step defined by '" + step + "'!");
- }
- }
-
- static String valueOf(Status status) {
- switch (status) {
- case unfinished : return "unfinished";
- case failed : return "failed";
- case succeeded : return "succeeded";
-
- default: throw new AssertionError("No value defined for '" + status + "'!");
- }
- }
-
- static Status stepStatusOf(String status) {
- switch (status) {
- case "unfinished" : return unfinished;
- case "failed" : return failed;
- case "succeeded" : return succeeded;
-
- default: throw new IllegalArgumentException("No status defined by '" + status + "'!");
- }
- }
-
- static Long valueOf(Instant instant) {
- return instant.toEpochMilli();
- }
-
- static String valueOf(RunStatus status) {
- return switch (status) {
- case running -> "running";
- case nodeAllocationFailure -> "nodeAllocationFailure";
- case endpointCertificateTimeout -> "endpointCertificateTimeout";
- case deploymentFailed -> "deploymentFailed";
- case invalidApplication -> "invalidApplication";
- case installationFailed -> "installationFailed";
- case testFailure -> "testFailure";
- case noTests -> "noTests";
- case error -> "error";
- case success -> "success";
- case aborted -> "aborted";
- case cancelled -> "cancelled";
- case reset -> "reset";
- case quotaExceeded -> "quotaExceeded";
- };
- }
-
- static RunStatus runStatusOf(String status) {
- return switch (status) {
- case "running" -> running;
- case "nodeAllocationFailure" -> nodeAllocationFailure;
- case "endpointCertificateTimeout" -> endpointCertificateTimeout;
- case "deploymentFailed" -> deploymentFailed;
- case "invalidApplication" -> invalidApplication;
- case "installationFailed" -> installationFailed;
- case "noTests" -> noTests;
- case "testFailure" -> testFailure;
- case "error" -> error;
- case "success" -> success;
- case "aborted" -> aborted;
- case "cancelled" -> cancelled;
- case "reset" -> reset;
- case "quotaExceeded" -> quotaExceeded;
- default -> throw new IllegalArgumentException("No run status defined by '" + status + "'!");
- };
- }
-
- Reason reasonFrom(Inspector object) {
- return new Reason(SlimeUtils.optionalString(object.field(reasonField)),
- Optional.ofNullable(jobIdFrom(object.field(dependentField))),
- Optional.ofNullable(toChange(object.field(changeField))));
- }
-
- void toSlime(Reason reason, Cursor object) {
- reason.reason().ifPresent(value -> object.setString(reasonField, value));
- reason.dependent().ifPresent(dependent -> toSlime(dependent, object.setObject(dependentField)));
- reason.change().ifPresent(change -> toSlime(change, object.setObject(changeField)));
- }
-
- JobId jobIdFrom(Inspector object) {
- if ( ! object.valid()) return null;
- return new JobId(ApplicationId.fromSerializedForm(object.field(applicationField).asString()),
- JobType.ofSerialized(object.field(jobTypeField).asString()));
- }
-
- void toSlime(JobId jobId, Cursor object) {
- object.setString(applicationField, jobId.application().serializedForm());
- object.setString(jobTypeField, jobId.type().serialized());
- }
-
- Change toChange(Inspector object) {
- if ( ! object.valid()) return null;
- Change change = Change.empty();
- if (object.field(platformVersionField).valid())
- change = change.with(Version.fromString(object.field(platformVersionField).asString()));
- if (object.field(buildField).valid())
- change = change.with(RevisionId.forProduction(object.field(buildField).asLong()));
- return change;
- }
-
- void toSlime(Change change, Cursor object) {
- change.platform().ifPresent(version -> object.setString(platformVersionField, version.toString()));
- change.revision().ifPresent(revision -> object.setLong(buildField, revision.number()));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializer.java
deleted file mode 100644
index 33f4709cfdd..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializer.java
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccess;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccessChange;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccessGrant;
-
-import java.time.Instant;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
-import java.util.List;
-import java.util.stream.Collectors;
-
-/**
- * (de)serializes support access status and history
- *
- * @author andreer
- */
-public class SupportAccessSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String stateFieldName = "state";
- private static final String supportAccessFieldName = "supportAccess";
- private static final String untilFieldName = "until";
- private static final String byFieldName = "by";
- private static final String historyFieldName = "history";
- private static final String allowedStateName = "allowed";
- private static final String disallowedStateName = "disallowed";
- private static final String atFieldName = "at";
- private static final String grantFieldName = "grants";
- private static final String requestorFieldName = "requestor";
- private static final String notBeforeFieldName = "notBefore";
- private static final String notAfterFieldName = "notAfter";
- private static final String certificateFieldName = "certificate";
-
-
- public static Slime toSlime(SupportAccess supportAccess) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
-
- serializeHistoricEvents(root, supportAccess.changeHistory(), List.of());
- serializeGrants(root, supportAccess.grantHistory(), true);
-
- return slime;
- }
-
- public static Slime serializeCurrentState(SupportAccess supportAccess, Instant currentTime) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
-
- Cursor status = root.setObject(stateFieldName);
- SupportAccess.CurrentStatus currentState = supportAccess.currentStatus(currentTime);
- status.setString(supportAccessFieldName, currentState.state().name());
- if (currentState.state() == SupportAccess.State.ALLOWED) {
- status.setString(untilFieldName, serializeInstant(currentState.allowedUntil().orElseThrow()));
- status.setString(byFieldName, currentState.allowedBy().orElseThrow());
- }
-
- List<SupportAccessGrant> inactiveGrants = supportAccess.grantHistory().stream()
- .filter(grant -> currentTime.isAfter(grant.certificate().getNotAfter().toInstant()))
- .toList();
-
- serializeHistoricEvents(root, supportAccess.changeHistory(), inactiveGrants);
-
- // Active grants should show up in the grant section
- List<SupportAccessGrant> activeGrants = supportAccess.grantHistory().stream()
- .filter(grant -> currentTime.isBefore(grant.certificate().getNotAfter().toInstant()))
- .toList();
- serializeGrants(root, activeGrants, false);
- return slime;
- }
-
- private static void serializeHistoricEvents(Cursor root, List<SupportAccessChange> changeEvents, List<SupportAccessGrant> historicGrants) {
- Cursor historyRoot = root.setArray(historyFieldName);
- for (SupportAccessChange change : changeEvents) {
- Cursor historyObject = historyRoot.addObject();
- historyObject.setString(stateFieldName, change.accessAllowedUntil().isPresent() ? allowedStateName : disallowedStateName);
- historyObject.setString(atFieldName, serializeInstant(change.changeTime()));
- change.accessAllowedUntil().ifPresent(allowedUntil -> historyObject.setString(untilFieldName, serializeInstant(allowedUntil)));
- historyObject.setString(byFieldName, change.madeBy());
- }
-
- for (SupportAccessGrant grant : historicGrants) {
- Cursor historyObject = historyRoot.addObject();
- historyObject.setString(stateFieldName, "grant");
- historyObject.setString(atFieldName, serializeInstant(grant.certificate().getNotBefore().toInstant()));
- historyObject.setString(untilFieldName, serializeInstant(grant.certificate().getNotAfter().toInstant()));
- historyObject.setString(byFieldName, grant.requestor());
- }
- }
-
- private static void serializeGrants(Cursor root, List<SupportAccessGrant> grants, boolean includeCertificates) {
- Cursor grantsRoot = root.setArray(grantFieldName);
- for (SupportAccessGrant grant : grants) {
- Cursor grantObject = grantsRoot.addObject();
- grantObject.setString(requestorFieldName, grant.requestor());
- if (includeCertificates) {
- grantObject.setString(certificateFieldName, X509CertificateUtils.toPem(grant.certificate()));
- }
- grantObject.setString(notBeforeFieldName, serializeInstant(grant.certificate().getNotBefore().toInstant()));
- grantObject.setString(notAfterFieldName, serializeInstant(grant.certificate().getNotAfter().toInstant()));
- }
-
- }
-
- private static String serializeInstant(Instant i) {
- return DateTimeFormatter.ISO_INSTANT.format(i.truncatedTo(ChronoUnit.SECONDS));
- }
-
- public static SupportAccess fromSlime(Slime slime) {
- List<SupportAccessGrant> grantHistory = SlimeUtils.entriesStream(slime.get().field(grantFieldName))
- .map(inspector ->
- new SupportAccessGrant(
- inspector.field(requestorFieldName).asString(),
- X509CertificateUtils.fromPem(inspector.field(certificateFieldName).asString())
- ))
- .toList();
-
- List<SupportAccessChange> changeHistory = SlimeUtils.entriesStream(slime.get().field(historyFieldName))
- .map(inspector ->
- new SupportAccessChange(
- SlimeUtils.optionalString(inspector.field(untilFieldName)).map(Instant::parse),
- Instant.parse(inspector.field(atFieldName).asString()),
- inspector.field(byFieldName).asString())
- )
- .toList();
-
- return new SupportAccess(changeHistory, grantHistory);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
deleted file mode 100644
index 961925cf620..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializer.java
+++ /dev/null
@@ -1,580 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.google.common.collect.BiMap;
-import com.google.common.collect.ImmutableBiMap;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.BillingInfo;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.BillingReference;
-import com.yahoo.vespa.hosted.controller.tenant.CloudAccountInfo;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Email;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.tenant.PurchaseOrder;
-import com.yahoo.vespa.hosted.controller.tenant.TaxId;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.tenant.TenantAddress;
-import com.yahoo.vespa.hosted.controller.tenant.TenantBilling;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContact;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-import com.yahoo.vespa.hosted.controller.tenant.TermsOfServiceApproval;
-
-import java.net.URI;
-import java.security.Principal;
-import java.security.PublicKey;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-/**
- * Slime serialization of {@link Tenant} sub-types.
- *
- * @author mpolden
- */
-public class TenantSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String nameField = "name";
- private static final String typeField = "type";
- private static final String athenzDomainField = "athenzDomain";
- private static final String propertyField = "property";
- private static final String propertyIdField = "propertyId";
- private static final String creatorField = "creator";
- private static final String createdAtField = "createdAt";
- private static final String deletedAtField = "deletedAt";
- private static final String contactField = "contact";
- private static final String contactUrlField = "contactUrl";
- private static final String propertyUrlField = "propertyUrl";
- private static final String issueTrackerUrlField = "issueTrackerUrl";
- private static final String personsField = "persons";
- private static final String personField = "person";
- private static final String queueField = "queue";
- private static final String componentField = "component";
- private static final String billingInfoField = "billingInfo";
- private static final String customerIdField = "customerId";
- private static final String productCodeField = "productCode";
- private static final String pemDeveloperKeysField = "pemDeveloperKeys";
- private static final String tenantInfoField = "info";
- private static final String lastLoginInfoField = "lastLoginInfo";
- private static final String secretStoresField = "secretStores";
- private static final String archiveAccessRoleField = "archiveAccessRole";
- private static final String archiveAccessField = "archiveAccess";
- private static final String awsArchiveAccessRoleField = "awsArchiveAccessRole";
- private static final String gcpArchiveAccessMemberField = "gcpArchiveAccessMember";
- private static final String invalidateUserSessionsBeforeField = "invalidateUserSessionsBefore";
- private static final String tenantRolesLastMaintainedField = "tenantRolesLastMaintained";
- private static final String billingReferenceField = "billingReference";
- private static final String planIdField = "planId";
- private static final String cloudAccountsField = "cloudAccounts";
- private static final String accountField = "account";
- private static final String templateVersionField = "templateVersion";
- private static final String taxIdField = "taxId";
- private static final String taxIdCountryField = "country";
- private static final String taxIdTypeField = "type";
- private static final String taxIdCodeField = "code";
- private static final String purchaseOrderField = "purchaseOrder";
- private static final String invoiceEmailField = "invoiceEmail";
- private static final String tosApprovalField = "tosApproval";
- private static final String tosApprovalAtField = "at";
- private static final String tosApprovalByField = "by";
-
- private static final String awsIdField = "awsId";
- private static final String roleField = "role";
-
- public Slime toSlime(Tenant tenant) {
- Slime slime = new Slime();
- Cursor tenantObject = slime.setObject();
- tenantObject.setString(nameField, tenant.name().value());
- tenantObject.setString(typeField, valueOf(tenant.type()));
- tenantObject.setLong(createdAtField, tenant.createdAt().toEpochMilli());
- toSlime(tenant.lastLoginInfo(), tenantObject.setObject(lastLoginInfoField));
- tenantObject.setLong(tenantRolesLastMaintainedField, tenant.tenantRolesLastMaintained().toEpochMilli());
- cloudAccountsToSlime(tenant.cloudAccounts(), tenantObject.setArray(cloudAccountsField));
-
- switch (tenant.type()) {
- case athenz: toSlime((AthenzTenant) tenant, tenantObject); break;
- case cloud: toSlime((CloudTenant) tenant, tenantObject); break;
- case deleted: toSlime((DeletedTenant) tenant, tenantObject); break;
- default: throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'.");
- }
- return slime;
- }
-
- private void toSlime(AthenzTenant tenant, Cursor tenantObject) {
- tenantObject.setString(athenzDomainField, tenant.domain().getName());
- tenantObject.setString(propertyField, tenant.property().id());
- tenant.propertyId().ifPresent(propertyId -> tenantObject.setString(propertyIdField, propertyId.id()));
- tenant.contact().ifPresent(contact -> {
- Cursor contactCursor = tenantObject.setObject(contactField);
- writeContact(contact, contactCursor);
- });
- }
-
- private void toSlime(CloudTenant tenant, Cursor root) {
- // BillingInfo was never used and always just a static default value. To retire this
- // field we continue to write the default value and stop reading it.
- // TODO(ogronnesby, 2020-08-05): Remove when a version where we do not read the field has propagated.
- var legacyBillingInfo = new BillingInfo("customer", "Vespa");
- tenant.creator().ifPresent(creator -> root.setString(creatorField, creator.getName()));
- developerKeysToSlime(tenant.developerKeys(), root.setArray(pemDeveloperKeysField));
- toSlime(legacyBillingInfo, root.setObject(billingInfoField));
- toSlime(tenant.info(), root);
- toSlime(tenant.tenantSecretStores(), root);
- toSlime(tenant.archiveAccess(), root);
- tenant.billingReference().ifPresent(b -> toSlime(b, root));
- tenant.invalidateUserSessionsBefore().ifPresent(instant -> root.setLong(invalidateUserSessionsBeforeField, instant.toEpochMilli()));
- root.setString(planIdField, tenant.planId().value());
- }
-
- private void toSlime(ArchiveAccess archiveAccess, Cursor root) {
- Cursor object = root.setObject(archiveAccessField);
- archiveAccess.awsRole().ifPresent(role -> object.setString(awsArchiveAccessRoleField, role));
- archiveAccess.gcpMember().ifPresent(member -> object.setString(gcpArchiveAccessMemberField, member));
- }
-
- private void toSlime(DeletedTenant tenant, Cursor root) {
- root.setLong(deletedAtField, tenant.deletedAt().toEpochMilli());
- }
-
- private void developerKeysToSlime(BiMap<PublicKey, ? extends Principal> keys, Cursor array) {
- keys.forEach((key, user) -> {
- Cursor object = array.addObject();
- object.setString("key", KeyUtils.toPem(key));
- object.setString("user", user.getName());
- });
- }
-
- private void toSlime(BillingInfo billingInfo, Cursor billingInfoObject) {
- billingInfoObject.setString(customerIdField, billingInfo.customerId());
- billingInfoObject.setString(productCodeField, billingInfo.productCode());
- }
-
- private void toSlime(LastLoginInfo lastLoginInfo, Cursor lastLoginInfoObject) {
- for (LastLoginInfo.UserLevel userLevel: LastLoginInfo.UserLevel.values()) {
- lastLoginInfo.get(userLevel).ifPresent(lastLoginAt ->
- lastLoginInfoObject.setLong(valueOf(userLevel), lastLoginAt.toEpochMilli()));
- }
- }
-
- private void cloudAccountsToSlime(List<CloudAccountInfo> cloudAccounts, Cursor cloudAccountsObject) {
- cloudAccounts.forEach(cloudAccountInfo -> {
- Cursor object = cloudAccountsObject.addObject();
- object.setString(accountField, cloudAccountInfo.cloudAccount().account());
- object.setString(templateVersionField, cloudAccountInfo.templateVersion().toFullString());
- });
- }
-
- public Tenant tenantFrom(Slime slime) {
- Inspector tenantObject = slime.get();
- Tenant.Type type = typeOf(tenantObject.field(typeField).asString());
-
- switch (type) {
- case athenz: return athenzTenantFrom(tenantObject);
- case cloud: return cloudTenantFrom(tenantObject);
- case deleted: return deletedTenantFrom(tenantObject);
- default: throw new IllegalArgumentException("Unexpected tenant type '" + type + "'.");
- }
- }
-
- private AthenzTenant athenzTenantFrom(Inspector tenantObject) {
- TenantName name = TenantName.from(tenantObject.field(nameField).asString());
- AthenzDomain domain = new AthenzDomain(tenantObject.field(athenzDomainField).asString());
- Property property = new Property(tenantObject.field(propertyField).asString());
- Optional<PropertyId> propertyId = SlimeUtils.optionalString(tenantObject.field(propertyIdField)).map(PropertyId::new);
- Optional<Contact> contact = contactFrom(tenantObject.field(contactField));
- Instant createdAt = SlimeUtils.instant(tenantObject.field(createdAtField));
- LastLoginInfo lastLoginInfo = lastLoginInfoFromSlime(tenantObject.field(lastLoginInfoField));
- Instant tenantRolesLastMaintained = SlimeUtils.instant(tenantObject.field(tenantRolesLastMaintainedField));
- List<CloudAccountInfo> cloudAccountInfos = cloudAccountsFromSlime(tenantObject.field(cloudAccountsField));
- return new AthenzTenant(name, domain, property, propertyId, contact, createdAt, lastLoginInfo, tenantRolesLastMaintained, cloudAccountInfos);
- }
-
- private CloudTenant cloudTenantFrom(Inspector tenantObject) {
- TenantName name = TenantName.from(tenantObject.field(nameField).asString());
- Instant createdAt = SlimeUtils.instant(tenantObject.field(createdAtField));
- LastLoginInfo lastLoginInfo = lastLoginInfoFromSlime(tenantObject.field(lastLoginInfoField));
- Optional<SimplePrincipal> creator = SlimeUtils.optionalString(tenantObject.field(creatorField)).map(SimplePrincipal::new);
- BiMap<PublicKey, SimplePrincipal> developerKeys = developerKeysFromSlime(tenantObject.field(pemDeveloperKeysField));
- TenantInfo info = tenantInfoFromSlime(tenantObject.field(tenantInfoField));
- List<TenantSecretStore> tenantSecretStores = secretStoresFromSlime(tenantObject.field(secretStoresField));
- ArchiveAccess archiveAccess = archiveAccessFromSlime(tenantObject);
- Optional<Instant> invalidateUserSessionsBefore = SlimeUtils.optionalInstant(tenantObject.field(invalidateUserSessionsBeforeField));
- Instant tenantRolesLastMaintained = SlimeUtils.instant(tenantObject.field(tenantRolesLastMaintainedField));
- List<CloudAccountInfo> cloudAccountInfos = cloudAccountsFromSlime(tenantObject.field(cloudAccountsField));
- Optional<BillingReference> billingReference = billingReferenceFrom(tenantObject.field(billingReferenceField));
- PlanId planId = planId(tenantObject.field(planIdField));
- return new CloudTenant(name, createdAt, lastLoginInfo, creator, developerKeys, info, tenantSecretStores,
- archiveAccess, invalidateUserSessionsBefore, tenantRolesLastMaintained,
- cloudAccountInfos, billingReference, planId);
- }
-
- private DeletedTenant deletedTenantFrom(Inspector tenantObject) {
- TenantName name = TenantName.from(tenantObject.field(nameField).asString());
- Instant createdAt = SlimeUtils.instant(tenantObject.field(createdAtField));
- Instant deletedAt = SlimeUtils.instant(tenantObject.field(deletedAtField));
- return new DeletedTenant(name, createdAt, deletedAt);
- }
-
- private BiMap<PublicKey, SimplePrincipal> developerKeysFromSlime(Inspector array) {
- ImmutableBiMap.Builder<PublicKey, SimplePrincipal> keys = ImmutableBiMap.builder();
- array.traverse((ArrayTraverser) (__, keyObject) ->
- keys.put(KeyUtils.fromPemEncodedPublicKey(keyObject.field("key").asString()),
- new SimplePrincipal(keyObject.field("user").asString())));
-
- return keys.build();
- }
-
- ArchiveAccess archiveAccessFromSlime(Inspector tenantObject) {
- // TODO(enygaard, 2022-05-24): Remove when all tenants have been rewritten to use ArchiveAccess object
- Optional<String> archiveAccessRole = SlimeUtils.optionalString(tenantObject.field(archiveAccessRoleField));
- if (archiveAccessRole.isPresent()) {
- return new ArchiveAccess().withAWSRole(archiveAccessRole.get());
- }
- Inspector object = tenantObject.field(archiveAccessField);
- if (!object.valid()) {
- return new ArchiveAccess();
- }
- Optional<String> awsArchiveAccessRole = SlimeUtils.optionalString(object.field(awsArchiveAccessRoleField));
- Optional<String> gcpArchiveAccessMember = SlimeUtils.optionalString(object.field(gcpArchiveAccessMemberField));
- return new ArchiveAccess()
- .withAWSRole(awsArchiveAccessRole)
- .withGCPMember(gcpArchiveAccessMember);
- }
-
- TenantInfo tenantInfoFromSlime(Inspector infoObject) {
- if (!infoObject.valid()) return TenantInfo.empty();
-
- return TenantInfo.empty()
- .withName(infoObject.field("name").asString())
- .withEmail(infoObject.field("email").asString())
- .withWebsite(infoObject.field("website").asString())
- .withContact(TenantContact.from(
- infoObject.field("contactName").asString(),
- new Email(infoObject.field("contactEmail").asString(), asBoolOrTrue(infoObject.field("contactEmailVerified")))))
- .withAddress(tenantInfoAddressFromSlime(infoObject.field("address")))
- .withBilling(tenantInfoBillingContactFromSlime(infoObject.field("billingContact")))
- .withContacts(tenantContactsFrom(infoObject.field("contacts")));
- }
-
- private TenantAddress tenantInfoAddressFromSlime(Inspector addressObject) {
- return TenantAddress.empty()
- .withAddress(addressObject.field("addressLines").asString())
- .withCode(addressObject.field("postalCodeOrZip").asString())
- .withCity(addressObject.field("city").asString())
- .withRegion(addressObject.field("stateRegionProvince").asString())
- .withCountry(addressObject.field("country").asString());
- }
-
- private TenantBilling tenantInfoBillingContactFromSlime(Inspector billingObject) {
- var taxIdInspector = billingObject.field(taxIdField);
- var taxId = switch (taxIdInspector.type()) {
- // TODO(bjorncs, 2023-11-02): Remove legacy tax id format
- case STRING -> TaxId.legacy(taxIdInspector.asString());
- case OBJECT -> {
- var taxIdCountry = taxIdInspector.field(taxIdCountryField).asString();
- var taxIdType = taxIdInspector.field(taxIdTypeField).asString();
- var taxIdCode = taxIdInspector.field(taxIdCodeField).asString();
- yield new TaxId(new TaxId.Country(taxIdCountry), new TaxId.Type(taxIdType), new TaxId.Code(taxIdCode));
- }
- case NIX -> TaxId.empty();
- default -> throw new IllegalStateException(taxIdInspector.type().name());
- };
- var purchaseOrder = new PurchaseOrder(billingObject.field(purchaseOrderField).asString());
- var invoiceEmail = new Email(billingObject.field(invoiceEmailField).asString(), false);
- var tosApprovalInspector = billingObject.field(tosApprovalField);
- var tosApproval = switch (tosApprovalInspector.type()) {
- case OBJECT -> new TermsOfServiceApproval(tosApprovalInspector.field(tosApprovalAtField).asString(),
- tosApprovalInspector.field(tosApprovalByField).asString());
- case NIX -> TermsOfServiceApproval.empty();
- default -> throw new IllegalArgumentException(taxIdInspector.type().name());
- };
-
- return TenantBilling.empty()
- .withContact(TenantContact.from(
- billingObject.field("name").asString(),
- new Email(billingObject.field("email").asString(), billingObject.field("emailVerified").asBool()),
- billingObject.field("phone").asString()))
- .withAddress(tenantInfoAddressFromSlime(billingObject.field("address")))
- .withTaxId(taxId)
- .withPurchaseOrder(purchaseOrder)
- .withInvoiceEmail(invoiceEmail)
- .withToSApproval(tosApproval);
- }
-
- private List<TenantSecretStore> secretStoresFromSlime(Inspector secretStoresObject) {
- if (!secretStoresObject.valid()) return List.of();
-
- return SlimeUtils.entriesStream(secretStoresObject)
- .map(inspector -> new TenantSecretStore(
- inspector.field(nameField).asString(),
- inspector.field(awsIdField).asString(),
- inspector.field(roleField).asString()))
- .toList();
- }
-
- private LastLoginInfo lastLoginInfoFromSlime(Inspector lastLoginInfoObject) {
- Map<LastLoginInfo.UserLevel, Instant> lastLoginByUserLevel = new HashMap<>();
- lastLoginInfoObject.traverse((String name, Inspector value) ->
- lastLoginByUserLevel.put(userLevelOf(name), SlimeUtils.instant(value)));
- return new LastLoginInfo(lastLoginByUserLevel);
- }
-
- private List<CloudAccountInfo> cloudAccountsFromSlime(Inspector cloudAccountsObject) {
- return SlimeUtils.entriesStream(cloudAccountsObject)
- .map(inspector -> new CloudAccountInfo(
- CloudAccount.from(inspector.field(accountField).asString()),
- Version.fromString(inspector.field(templateVersionField).asString())))
- .toList();
- }
-
- void toSlime(TenantInfo info, Cursor parentCursor) {
- if (info.isEmpty()) return;
- Cursor infoCursor = parentCursor.setObject("info");
- infoCursor.setString("name", info.name());
- infoCursor.setString("email", info.email());
- infoCursor.setString("website", info.website());
- infoCursor.setString("contactName", info.contact().name());
- infoCursor.setString("contactEmail", info.contact().email().getEmailAddress());
- infoCursor.setBool("contactEmailVerified", info.contact().email().isVerified());
- toSlime(info.address(), infoCursor);
- toSlime(info.billingContact(), infoCursor);
- toSlime(info.contacts(), infoCursor);
- }
-
- private void toSlime(TenantAddress address, Cursor parentCursor) {
- if (address.isEmpty()) return;
-
- Cursor addressCursor = parentCursor.setObject("address");
- addressCursor.setString("addressLines", address.address());
- addressCursor.setString("postalCodeOrZip", address.code());
- addressCursor.setString("city", address.city());
- addressCursor.setString("stateRegionProvince", address.region());
- addressCursor.setString("country", address.country());
- }
-
- private void toSlime(TenantBilling billingContact, Cursor parentCursor) {
- if (billingContact.isEmpty()) return;
-
- Cursor billingCursor = parentCursor.setObject("billingContact");
- billingCursor.setString("name", billingContact.contact().name());
- billingCursor.setString("email", billingContact.contact().email().getEmailAddress());
- billingCursor.setBool("emailVerified", billingContact.contact().email().isVerified());
- billingCursor.setString("phone", billingContact.contact().phone());
- var taxIdCursor = billingCursor.setObject(taxIdField);
- taxIdCursor.setString(taxIdCountryField, billingContact.getTaxId().country().value());
- taxIdCursor.setString(taxIdTypeField, billingContact.getTaxId().type().value());
- taxIdCursor.setString(taxIdCodeField, billingContact.getTaxId().code().value());
- billingCursor.setString(purchaseOrderField, billingContact.getPurchaseOrder().value());
- billingCursor.setString(invoiceEmailField, billingContact.getInvoiceEmail().getEmailAddress());
- toSlime(billingContact.address(), billingCursor);
- if (!billingContact.getToSApproval().isEmpty()) {
- var tosApprovalCursor = billingCursor.setObject(tosApprovalField);
- tosApprovalCursor.setString(tosApprovalAtField, billingContact.getToSApproval().approvedAt().toString());
- tosApprovalCursor.setString(tosApprovalByField, billingContact.getToSApproval().approvedBy().get().getName());
- }
- }
-
- private void toSlime(List<TenantSecretStore> tenantSecretStores, Cursor parentCursor) {
- if (tenantSecretStores.isEmpty()) return;
-
- Cursor secretStoresCursor = parentCursor.setArray(secretStoresField);
- tenantSecretStores.forEach(tenantSecretStore -> {
- Cursor secretStoreCursor = secretStoresCursor.addObject();
- secretStoreCursor.setString(nameField, tenantSecretStore.getName());
- secretStoreCursor.setString(awsIdField, tenantSecretStore.getAwsId());
- secretStoreCursor.setString(roleField, tenantSecretStore.getRole());
- });
- }
-
- private void toSlime(TenantContacts contacts, Cursor parent) {
- if (contacts.isEmpty()) return;
- var cursor = parent.setArray("contacts");
- contacts.all().forEach(contact -> writeContact(contact, cursor.addObject()));
- }
-
- private void toSlime(BillingReference reference, Cursor parent) {
- var cursor = parent.setObject(billingReferenceField);
- cursor.setString("reference", reference.reference());
- cursor.setLong("updated", reference.updated().toEpochMilli());
- }
-
- private Optional<BillingReference> billingReferenceFrom(Inspector object) {
- if (! object.valid()) return Optional.empty();
- return Optional.of(new BillingReference(
- object.field("reference").asString(),
- SlimeUtils.instant(object.field("updated"))));
- }
-
- private PlanId planId(Inspector object) {
- if (! object.valid()) return PlanId.from("none");
-
- return PlanId.from(object.asString());
- }
-
- private TenantContacts tenantContactsFrom(Inspector object) {
- List<TenantContacts.Contact> contacts = SlimeUtils.entriesStream(object)
- .map(this::readContact)
- .toList();
- return new TenantContacts(contacts);
- }
-
- private Optional<Contact> contactFrom(Inspector object) {
- if ( ! object.valid()) return Optional.empty();
-
- URI contactUrl = URI.create(object.field(contactUrlField).asString());
- URI propertyUrl = URI.create(object.field(propertyUrlField).asString());
- URI issueTrackerUrl = URI.create(object.field(issueTrackerUrlField).asString());
- List<List<String>> persons = personsFrom(object.field(personsField));
- String queue = object.field(queueField).asString();
- Optional<String> component = object.field(componentField).valid() ? Optional.of(object.field(componentField).asString()) : Optional.empty();
- return Optional.of(new Contact(contactUrl,
- propertyUrl,
- issueTrackerUrl,
- persons,
- queue,
- component));
- }
-
- private void writeContact(Contact contact, Cursor contactCursor) {
- contactCursor.setString(contactUrlField, contact.url().toString());
- contactCursor.setString(propertyUrlField, contact.propertyUrl().toString());
- contactCursor.setString(issueTrackerUrlField, contact.issueTrackerUrl().toString());
- Cursor personsArray = contactCursor.setArray(personsField);
- contact.persons().forEach(personList -> {
- Cursor personArray = personsArray.addArray();
- personList.forEach(person -> {
- Cursor personObject = personArray.addObject();
- personObject.setString(personField, person);
- });
- });
- contactCursor.setString(queueField, contact.queue());
- contact.component().ifPresent(component -> contactCursor.setString(componentField, component));
- }
-
- private List<List<String>> personsFrom(Inspector array) {
- List<List<String>> personLists = new ArrayList<>();
- array.traverse((ArrayTraverser) (i, personArray) -> {
- List<String> persons = new ArrayList<>();
- personArray.traverse((ArrayTraverser) (j, inspector) -> persons.add(inspector.field("person").asString()));
- personLists.add(persons);
- });
- return personLists;
- }
-
- private void writeContact(TenantContacts.Contact contact, Cursor cursor) {
- cursor.setString("type", contact.type().value());
- Cursor audiencesArray = cursor.setArray("audiences");
- contact.audiences().forEach(audience -> audiencesArray.addString(toAudience(audience)));
- var data = cursor.setObject("data");
- switch (contact.type()) {
- case EMAIL:
- var email = (TenantContacts.EmailContact) contact;
- data.setString("email", email.email().getEmailAddress());
- data.setBool("emailVerified", email.email().isVerified());
- return;
- default:
- throw new IllegalArgumentException("Serialization for contact type not implemented: " + contact.type());
- }
- }
-
- private TenantContacts.Contact readContact(Inspector inspector) {
- var type = TenantContacts.Type.from(inspector.field("type").asString())
- .orElseThrow(() -> new RuntimeException("Unknown type: " + inspector.field("type").asString()));
- var audiences = SlimeUtils.entriesStream(inspector.field("audiences"))
- .map(audience -> TenantSerializer.fromAudience(audience.asString()))
- .toList();
- switch (type) {
- case EMAIL:
- var isVerified = asBoolOrTrue(inspector.field("data").field("emailVerified"));
- return new TenantContacts.EmailContact(audiences, new Email(inspector.field("data").field("email").asString(), isVerified));
- default:
- throw new IllegalArgumentException("Serialization for contact type not implemented: " + type);
- }
-
- }
-
- private static Tenant.Type typeOf(String value) {
- switch (value) {
- case "athenz": return Tenant.Type.athenz;
- case "cloud": return Tenant.Type.cloud;
- case "deleted": return Tenant.Type.deleted;
- default: throw new IllegalArgumentException("Unknown tenant type '" + value + "'.");
- }
- }
-
- private static String valueOf(Tenant.Type type) {
- switch (type) {
- case athenz: return "athenz";
- case cloud: return "cloud";
- case deleted: return "deleted";
- default: throw new IllegalArgumentException("Unexpected tenant type '" + type + "'.");
- }
- }
-
- private static LastLoginInfo.UserLevel userLevelOf(String value) {
- switch (value) {
- case "user": return LastLoginInfo.UserLevel.user;
- case "developer": return LastLoginInfo.UserLevel.developer;
- case "administrator": return LastLoginInfo.UserLevel.administrator;
- default: throw new IllegalArgumentException("Unknown user level '" + value + "'.");
- }
- }
-
- private static String valueOf(LastLoginInfo.UserLevel userLevel) {
- switch (userLevel) {
- case user: return "user";
- case developer: return "developer";
- case administrator: return "administrator";
- default: throw new IllegalArgumentException("Unexpected user level '" + userLevel + "'.");
- }
- }
-
- private static TenantContacts.Audience fromAudience(String value) {
- switch (value) {
- case "tenant": return TenantContacts.Audience.TENANT;
- case "notifications": return TenantContacts.Audience.NOTIFICATIONS;
- default: throw new IllegalArgumentException("Unknown contact audience '" + value + "'.");
- }
- }
-
- private static String toAudience(TenantContacts.Audience audience) {
- switch (audience) {
- case TENANT: return "tenant";
- case NOTIFICATIONS: return "notifications";
- default: throw new IllegalArgumentException("Unexpected contact audience '" + audience + "'.");
- }
- }
-
- private boolean asBoolOrTrue(Inspector inspector) {
- return !inspector.valid() || inspector.asBool();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TrialNotifications.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TrialNotifications.java
deleted file mode 100644
index 4ea4fe79e8f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/TrialNotifications.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.logging.Logger;
-
-/**
- * @author bjorncs
- */
-public record TrialNotifications(List<Status> tenants) {
- private static final Logger log = Logger.getLogger(TrialNotifications.class.getName());
-
- public TrialNotifications { tenants = List.copyOf(tenants); }
-
- public record Status(TenantName tenant, State state, Instant lastUpdate) {}
- public enum State { SIGNED_UP, MID_CHECK_IN, EXPIRES_IMMEDIATELY, EXPIRED, UNKNOWN }
-
- public Slime toSlime() {
- var slime = new Slime();
- var rootCursor = slime.setObject();
- var tenantsCursor = rootCursor.setArray("tenants");
- for (Status t : tenants) {
- var tenantCursor = tenantsCursor.addObject();
- tenantCursor.setString("tenant", t.tenant().value());
- tenantCursor.setString("state", t.state().name());
- tenantCursor.setString("lastUpdate", t.lastUpdate().toString());
- }
- log.fine(() -> "Generated json '%s' from '%s'".formatted(SlimeUtils.toJson(slime), this));
- return slime;
- }
-
- public static TrialNotifications fromSlime(Slime slime) {
- var rootCursor = slime.get();
- var tenantsCursor = rootCursor.field("tenants");
- var tenants = new ArrayList<Status>();
- for (int i = 0; i < tenantsCursor.entries(); i++) {
- var tenantCursor = tenantsCursor.entry(i);
- var name = TenantName.from(tenantCursor.field("tenant").asString());
- var stateStr = tenantCursor.field("state").asString();
- var state = Arrays.stream(State.values())
- .filter(s -> s.name().equals(stateStr)).findFirst().orElse(State.UNKNOWN);
- var lastUpdate = Instant.parse(tenantCursor.field("lastUpdate").asString());
- tenants.add(new Status(name, state, lastUpdate));
- }
- var tn = new TrialNotifications(tenants);
- log.fine(() -> "Parsed '%s' from '%s'".formatted(tn, SlimeUtils.toJson(slime)));
- return tn;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/UnassignedCertificateSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/UnassignedCertificateSerializer.java
deleted file mode 100644
index 44f50800561..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/UnassignedCertificateSerializer.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.certificate.UnassignedCertificate;
-
-/**
- * @author mpolden
- */
-public class UnassignedCertificateSerializer {
-
- private static final String stateKey = "state";
- private static final String certificateKey = "certificate";
-
- public Slime toSlime(UnassignedCertificate unassignedCertificate) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- root.setString(stateKey, unassignedCertificate.state().name());
- EndpointCertificateSerializer.toSlime(unassignedCertificate.certificate(), root.setObject(certificateKey));
- return slime;
- }
-
- public UnassignedCertificate fromSlime(Slime slime) {
- Cursor root = slime.get();
- UnassignedCertificate.State state = UnassignedCertificate.State.valueOf(root.field(stateKey).asString());
- EndpointCertificate certificate = EndpointCertificateSerializer.fromSlime(root.field(certificateKey));
- return new UnassignedCertificate(certificate, state);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java
deleted file mode 100644
index e4de073e2c6..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializer.java
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-
-/**
- * Serializer for {@link VersionStatus}.
- *
- * @author mpolden
- */
-public class VersionStatusSerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- // VersionStatus fields
- private static final String versionsField = "versions";
- private static final String currentMajorField = "currentMajor";
-
- // VespaVersion fields
- private static final String releaseCommitField = "releaseCommit";
- private static final String committedAtField = "releasedAt";
- private static final String isControllerVersionField = "isCurrentControllerVersion";
- private static final String isSystemVersionField = "isCurrentSystemVersion";
- private static final String isReleasedField = "isReleased";
- private static final String deploymentStatisticsField = "deploymentStatistics";
- private static final String confidenceField = "confidence";
-
- // NodeVersions fields
- private static final String nodeVersionsField = "nodeVersions";
-
- // DeploymentStatistics fields
- private static final String versionField = "version";
- private static final String failingField = "failing";
- private static final String productionField = "production";
- private static final String deployingField = "deploying";
-
- private final NodeVersionSerializer nodeVersionSerializer;
-
- public VersionStatusSerializer(NodeVersionSerializer nodeVersionSerializer) {
- this.nodeVersionSerializer = Objects.requireNonNull(nodeVersionSerializer, "nodeVersionSerializer must be non-null");
- }
-
- public Slime toSlime(VersionStatus status) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- versionsToSlime(status.versions(), root.setArray(versionsField));
- root.setLong(currentMajorField, status.currentMajor());
- return slime;
- }
-
- public VersionStatus fromSlime(Slime slime) {
- Inspector root = slime.get();
- return new VersionStatus(vespaVersionsFromSlime(root.field(versionsField)),
- (int) root.field(currentMajorField).asLong());
- }
-
- private void versionsToSlime(List<VespaVersion> versions, Cursor array) {
- versions.forEach(version -> vespaVersionToSlime(version, array.addObject()));
- }
-
- private void vespaVersionToSlime(VespaVersion version, Cursor object) {
- object.setString(releaseCommitField, version.releaseCommit());
- object.setLong(committedAtField, version.committedAt().toEpochMilli());
- object.setBool(isControllerVersionField, version.isControllerVersion());
- object.setBool(isSystemVersionField, version.isSystemVersion());
- object.setBool(isReleasedField, version.isReleased());
- deploymentStatisticsToSlime(version.versionNumber(), object.setObject(deploymentStatisticsField));
- object.setString(confidenceField, version.confidence().name());
- nodeVersionsToSlime(version.nodeVersions(), object.setArray(nodeVersionsField));
- }
-
- private void nodeVersionsToSlime(List<NodeVersion> nodeVersions, Cursor array) {
- nodeVersionSerializer.nodeVersionsToSlime(nodeVersions, array);
- }
-
- private void deploymentStatisticsToSlime(Version version, Cursor object) {
- object.setString(versionField, version.toString());
- // TODO jonmv: Remove the below.
- object.setArray(failingField);
- object.setArray(productionField);
- object.setArray(deployingField);
- }
-
- private List<VespaVersion> vespaVersionsFromSlime(Inspector array) {
- List<VespaVersion> versions = new ArrayList<>();
- array.traverse((ArrayTraverser) (i, object) -> versions.add(vespaVersionFromSlime(object)));
- return Collections.unmodifiableList(versions);
- }
-
- private VespaVersion vespaVersionFromSlime(Inspector object) {
- var version = Version.fromString(object.field(deploymentStatisticsField).field(versionField).asString());
- return new VespaVersion(version,
- object.field(releaseCommitField).asString(),
- SlimeUtils.instant(object.field(committedAtField)),
- object.field(isControllerVersionField).asBool(),
- object.field(isSystemVersionField).asBool(),
- object.field(isReleasedField).asBool(),
- nodeVersionSerializer.nodeVersionsFromSlime(object.field(nodeVersionsField), version),
- VespaVersion.Confidence.valueOf(object.field(confidenceField).asString())
- );
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ZoneRoutingPolicySerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ZoneRoutingPolicySerializer.java
deleted file mode 100644
index 97b0e340025..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/ZoneRoutingPolicySerializer.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.routing.ZoneRoutingPolicy;
-
-import java.util.Objects;
-
-/**
- * Serializer for {@link ZoneRoutingPolicy}.
- *
- * @author mpolden
- */
-public class ZoneRoutingPolicySerializer {
-
- // WARNING: Since there are multiple servers in a ZooKeeper cluster and they upgrade one by one
- // (and rewrite all nodes on startup), changes to the serialized format must be made
- // such that what is serialized on version N+1 can be read by version N:
- // - ADDING FIELDS: Always ok
- // - REMOVING FIELDS: Stop reading the field first. Stop writing it on a later version.
- // - CHANGING THE FORMAT OF A FIELD: Don't do it bro.
-
- private static final String GLOBAL_ROUTING_FIELD = "globalRouting";
-
- private final RoutingPolicySerializer routingPolicySerializer;
-
- public ZoneRoutingPolicySerializer(RoutingPolicySerializer routingPolicySerializer) {
- this.routingPolicySerializer = Objects.requireNonNull(routingPolicySerializer, "routingPolicySerializer must be non-null");
- }
-
- public ZoneRoutingPolicy fromSlime(ZoneId zone, Slime slime) {
- var root = slime.get();
- return new ZoneRoutingPolicy(zone, routingPolicySerializer.routingStatusFromSlime(root.field(GLOBAL_ROUTING_FIELD)));
- }
-
- public Slime toSlime(ZoneRoutingPolicy policy) {
- var slime = new Slime();
- var root = slime.setObject();
- routingPolicySerializer.globalRoutingToSlime(policy.routingStatus(), root.setObject(GLOBAL_ROUTING_FIELD));
- return slime;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/package-info.java
deleted file mode 100644
index abb8ab08d89..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/persistence/package-info.java
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * Persistence layer for the controller.
- *
- * @author bratseth
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutor.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutor.java
deleted file mode 100644
index e623b7e440c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutor.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.proxy;
-
-import com.yahoo.container.jdisc.HttpResponse;
-
-/**
- * Executes call against config servers and handles discovery requests. Rest URIs in the response are
- * rewritten.
- *
- * @author Haakon Dybdahl
- */
-public interface ConfigServerRestExecutor {
-
- HttpResponse handle(ProxyRequest request);
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java
deleted file mode 100644
index bbed0554350..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImpl.java
+++ /dev/null
@@ -1,301 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.proxy;
-
-import ai.vespa.util.http.hc4.SslConnectionSocketFactory;
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.jdisc.http.HttpRequest.Method;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier;
-import com.yahoo.vespa.hosted.controller.api.integration.ControllerIdentityProvider;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.yolean.concurrent.Sleeper;
-import org.apache.http.Header;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpDelete;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPatch;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpPut;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
-import org.apache.http.entity.InputStreamEntity;
-import org.apache.http.impl.DefaultConnectionReuseStrategy;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.apache.http.protocol.HttpContext;
-import org.apache.http.protocol.HttpCoreContext;
-import org.apache.http.util.EntityUtils;
-
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLSession;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.net.URI;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-import static com.yahoo.yolean.Exceptions.uncheck;
-
-
-/**
- * @author Haakon Dybdahl
- * @author bjorncs
- */
-@SuppressWarnings("unused") // Injected
-public class ConfigServerRestExecutorImpl extends AbstractComponent implements ConfigServerRestExecutor {
-
- private static final Logger LOG = Logger.getLogger(ConfigServerRestExecutorImpl.class.getName());
- private static final Duration PROXY_REQUEST_TIMEOUT = Duration.ofSeconds(20);
- private static final Duration PING_REQUEST_TIMEOUT = Duration.ofMillis(500);
- private static final Duration SINGLE_TARGET_WAIT = Duration.ofSeconds(2);
- private static final int SINGLE_TARGET_RETRIES = 3;
- private static final Set<String> HEADERS_TO_COPY = Set.of("X-HTTP-Method-Override", "Content-Type");
-
- private final CloseableHttpClient client;
- private final Sleeper sleeper;
-
- @Inject
- public ConfigServerRestExecutorImpl(ZoneRegistry zoneRegistry, ControllerIdentityProvider identityProvider) {
- this(SslConnectionSocketFactory.of(identityProvider.getConfigServerSslSocketFactory(), new ControllerOrConfigserverHostnameVerifier(zoneRegistry)),
- Sleeper.DEFAULT, // Specify
- new ConnectionReuseStrategy(zoneRegistry));
- }
-
- ConfigServerRestExecutorImpl(SSLConnectionSocketFactory connectionSocketFactory,
- Sleeper sleeper, ConnectionReuseStrategy connectionReuseStrategy) {
- this.client = createHttpClient(connectionSocketFactory, connectionReuseStrategy);
- this.sleeper = sleeper;
- }
-
- @Override
- public ProxyResponse handle(ProxyRequest request) {
- List<URI> targets = new ArrayList<>(request.getTargets());
-
- StringBuilder errorBuilder = new StringBuilder();
- boolean singleTarget = targets.size() == 1;
- if (singleTarget) {
- for (int i = 0; i < SINGLE_TARGET_RETRIES - 1; i++) {
- targets.add(targets.get(0));
- }
- } else if (queueFirstServerIfDown(targets)) {
- errorBuilder.append("Change ordering due to failed ping.");
- }
-
- for (URI url : targets) {
- Optional<ProxyResponse> proxyResponse = proxy(request, url, errorBuilder);
- if (proxyResponse.isPresent()) {
- return proxyResponse.get();
- }
- if (singleTarget) {
- sleeper.sleep(SINGLE_TARGET_WAIT);
- }
- }
-
- throw new RuntimeException("Failed talking to config servers: " + errorBuilder);
- }
-
- private Optional<ProxyResponse> proxy(ProxyRequest request, URI url, StringBuilder errorBuilder) {
- HttpRequestBase requestBase = createHttpBaseRequest(
- request.getMethod(), request.createConfigServerRequestUri(url), request.getData());
- // Empty list of headers to copy for now, add headers when needed, or rewrite logic.
- copyHeaders(request.getHeaders(), requestBase);
-
- try (CloseableHttpResponse response = client.execute(requestBase)) {
- String content = getContent(response);
- int status = response.getStatusLine().getStatusCode();
- if (status / 100 == 5) {
- errorBuilder.append("Talking to server ").append(url.getHost())
- .append(", got ").append(status).append(" ")
- .append(content).append("\n");
- LOG.log(Level.FINE, () -> Text.format("Got response from %s with status code %d and content:\n %s",
- url.getHost(), status, content));
- return Optional.empty();
- }
- Header contentHeader = response.getLastHeader("Content-Type");
- String contentType;
- if (contentHeader != null && contentHeader.getValue() != null && ! contentHeader.getValue().isEmpty()) {
- contentType = contentHeader.getValue().replace("; charset=UTF-8","");
- } else {
- contentType = "application/json";
- }
- // Send response back
- return Optional.of(new ProxyResponse(request, content, status, url, contentType));
- } catch (Exception e) {
- errorBuilder.append("Talking to server ").append(url.getHost())
- .append(" got exception ").append(e.getMessage())
- .append("\n");
- LOG.log(Level.FINE, e, () -> "Got exception while sending request to " + url.getHost());
- return Optional.empty();
- }
- }
-
- private static String getContent(CloseableHttpResponse response) {
- return Optional.ofNullable(response.getEntity())
- .map(entity -> uncheck(() -> EntityUtils.toString(entity)))
- .orElse("");
- }
-
- private static HttpRequestBase createHttpBaseRequest(Method method, URI url, InputStream data) {
- switch (method) {
- case GET:
- return new HttpGet(url);
- case POST:
- HttpPost post = new HttpPost(url);
- if (data != null) {
- post.setEntity(new InputStreamEntity(data));
- }
- return post;
- case PUT:
- HttpPut put = new HttpPut(url);
- if (data != null) {
- put.setEntity(new InputStreamEntity(data));
- }
- return put;
- case DELETE:
- return new HttpDelete(url);
- case PATCH:
- HttpPatch patch = new HttpPatch(url);
- if (data != null) {
- patch.setEntity(new InputStreamEntity(data));
- }
- return patch;
- }
- throw new IllegalArgumentException("Refusing to proxy " + method + " " + url + ": Unsupported method");
- }
-
- private static void copyHeaders(Map<String, List<String>> headers, HttpRequestBase toRequest) {
- for (Map.Entry<String, List<String>> headerEntry : headers.entrySet()) {
- if (HEADERS_TO_COPY.contains(headerEntry.getKey())) {
- for (String value : headerEntry.getValue()) {
- toRequest.addHeader(headerEntry.getKey(), value);
- }
- }
- }
- }
-
- /**
- * During upgrade, one server can be down, this is normal. Therefore we do a quick ping on the first server,
- * if it is not responding, we try the other servers first. False positive/negatives are not critical,
- * but will increase latency to some extent.
- */
- private boolean queueFirstServerIfDown(List<URI> allServers) {
- if (allServers.size() < 2) {
- return false;
- }
- URI uri = allServers.get(0);
- HttpGet httpGet = new HttpGet(uri);
-
- RequestConfig config = RequestConfig.custom()
- .setConnectTimeout((int) PING_REQUEST_TIMEOUT.toMillis())
- .setConnectionRequestTimeout((int) PING_REQUEST_TIMEOUT.toMillis())
- .setSocketTimeout((int) PING_REQUEST_TIMEOUT.toMillis()).build();
- httpGet.setConfig(config);
-
- try (CloseableHttpResponse response = client.execute(httpGet)) {
- if (response.getStatusLine().getStatusCode() == 200) {
- return false;
- }
-
- } catch (IOException e) {
- // We ignore this, if server is restarting this might happen.
- }
- // Some error happened, move this server to the back. The other servers should be running.
- Collections.rotate(allServers, -1);
- return true;
- }
-
- @Override
- public void deconstruct() {
- try {
- client.close();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- private static CloseableHttpClient createHttpClient(SSLConnectionSocketFactory connectionSocketFactory,
- org.apache.http.ConnectionReuseStrategy connectionReuseStrategy) {
-
- RequestConfig config = RequestConfig.custom()
- .setConnectTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis())
- .setConnectionRequestTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis())
- .setSocketTimeout((int) PROXY_REQUEST_TIMEOUT.toMillis()).build();
- return HttpClientBuilder.create()
- .setUserAgent("config-server-proxy-client")
- .setSSLSocketFactory(connectionSocketFactory)
- .setDefaultRequestConfig(config)
- .setMaxConnPerRoute(10)
- .setMaxConnTotal(500)
- .setConnectionReuseStrategy(connectionReuseStrategy)
- .setConnectionTimeToLive(1, TimeUnit.MINUTES)
- .build();
-
- }
-
- private static class ControllerOrConfigserverHostnameVerifier implements HostnameVerifier {
-
- private final HostnameVerifier configserverVerifier;
-
- ControllerOrConfigserverHostnameVerifier(ZoneRegistry registry) {
- this.configserverVerifier = createConfigserverVerifier(registry);
- }
-
- private static HostnameVerifier createConfigserverVerifier(ZoneRegistry registry) {
- Set<AthenzIdentity> configserverIdentities = registry.zones().all().zones().stream()
- .map(zone -> registry.getConfigServerHttpsIdentity(zone.getId()))
- .collect(Collectors.toSet());
- return new AthenzIdentityVerifier(configserverIdentities);
- }
-
- @Override
- public boolean verify(String hostname, SSLSession session) {
- return "localhost".equals(hostname) || configserverVerifier.verify(hostname, session);
- }
- }
-
- /**
- * A connection reuse strategy which avoids reusing connections to VIPs. Since VIPs are TCP-level load balancers,
- * a reconnect is needed to (potentially) switch real server.
- */
- public static class ConnectionReuseStrategy extends DefaultConnectionReuseStrategy {
-
- private final Set<String> vips;
-
- public ConnectionReuseStrategy(ZoneRegistry zoneRegistry) {
- this(zoneRegistry.zones().all().ids().stream()
- .map(zoneRegistry::getConfigServerVipUri)
- .map(URI::getHost)
- .collect(Collectors.toUnmodifiableSet()));
- }
-
- public ConnectionReuseStrategy(Set<String> vips) {
- this.vips = Set.copyOf(vips);
- }
-
- @Override
- public boolean keepAlive(HttpResponse response, HttpContext context) {
- HttpCoreContext coreContext = HttpCoreContext.adapt(context);
- String host = coreContext.getTargetHost().getHostName();
- if (vips.contains(host)) {
- return false;
- }
- return super.keepAlive(response, context);
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java
deleted file mode 100644
index 2a29e2b590d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequest.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.proxy;
-
-import ai.vespa.http.HttpURL;
-import ai.vespa.http.HttpURL.Path;
-import ai.vespa.http.HttpURL.Query;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.text.Text;
-
-import java.io.InputStream;
-import java.net.URI;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-
-import static com.yahoo.jdisc.http.HttpRequest.Method;
-
-/**
- * Keeping information about the calls that are being proxied.
- * A request is of form /zone/v2/[environment]/[region]/[config-server-path]
- *
- * @author Haakon Dybdahl
- */
-public class ProxyRequest {
-
- private final Method method;
- private final HttpURL requestUri;
- private final Map<String, List<String>> headers;
- private final InputStream requestData;
-
- private final List<URI> targets;
- private final Path targetPath;
-
- ProxyRequest(Method method, URI uri, Map<String, List<String>> headers, InputStream body, List<URI> targets, Path path) {
- this.requestUri = HttpURL.from(uri);
- if ( requestUri.path().length() < path.length()
- || ! requestUri.path().tail(path.length()).equals(path)) {
- throw new IllegalArgumentException(Text.format("Request %s does not end with proxy %s", requestUri.path(), path));
- }
- if (targets.isEmpty()) {
- throw new IllegalArgumentException("targets must be non-empty");
- }
- this.method = Objects.requireNonNull(method);
- this.headers = Objects.requireNonNull(headers);
- this.requestData = body;
- this.targets = List.copyOf(targets);
- this.targetPath = path;
- }
-
-
- public Method getMethod() {
- return method;
- }
-
- public Map<String, List<String>> getHeaders() {
- return headers;
- }
-
- public InputStream getData() {
- return requestData;
- }
-
- public List<URI> getTargets() {
- return targets;
- }
-
- public URI createConfigServerRequestUri(URI baseURI) {
- return HttpURL.from(baseURI).withPath(targetPath).withQuery(requestUri.query()).asURI();
- }
-
- public URI getControllerPrefixUri() {
- Path prefixPath = requestUri.path().cut(targetPath.length()).withTrailingSlash();
- return requestUri.withPath(prefixPath).withQuery(Query.empty()).asURI();
- }
-
- @Override
- public String toString() {
- return "[targets: " + targets + " request: " + targetPath + "]";
- }
-
- /** Create a proxy request that repeatedly tries a single target */
- public static ProxyRequest tryOne(URI target, Path path, HttpRequest request) {
- return new ProxyRequest(request.getMethod(), request.getUri(), request.getJDiscRequest().headers(),
- request.getData(), List.of(target), path);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java
deleted file mode 100644
index caf2ff05814..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponse.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.proxy;
-
-import ai.vespa.http.HttpURL;
-import ai.vespa.http.HttpURL.Path;
-import com.yahoo.container.jdisc.HttpResponse;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-
-/**
- * Response class that also rewrites URL from config server.
- *
- * @author Haakon Dybdahl
- */
-public class ProxyResponse extends HttpResponse {
-
- private final String bodyResponseRewritten;
- private final String contentType;
-
- public ProxyResponse(
- ProxyRequest controllerRequest,
- String bodyResponse,
- int statusResponse,
- URI configServer,
- String contentType) {
- super(statusResponse);
- this.contentType = contentType;
-
- // Configserver always serves from 4443, therefore all responses will have port 4443 in them,
- // but the request URI (loadbalancer/VIP) is not always 4443
- String configServerPrefix = HttpURL.from(configServer).withPort(4443).withPath(Path.empty()).asURI().toString();
- String controllerRequestPrefix = controllerRequest.getControllerPrefixUri().toString();
- bodyResponseRewritten = bodyResponse.replace(configServerPrefix, controllerRequestPrefix);
- }
-
- @Override
- public void render(OutputStream stream) throws IOException {
- stream.write(bodyResponseRewritten.getBytes(StandardCharsets.UTF_8));
- }
-
- @Override
- public String getContentType() { return contentType; }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/package-info.java
deleted file mode 100644
index 0acb064f52a..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/proxy/package-info.java
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author Haakon Dybdahl
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.proxy;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/ErrorResponses.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/ErrorResponses.java
deleted file mode 100644
index 56844887caf..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/ErrorResponses.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi;
-
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.restapi.ErrorResponse;
-
-import java.util.UUID;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * Helper class for creating error responses.
- *
- * @author mpolden
- */
-public class ErrorResponses {
-
- private ErrorResponses() {}
-
- /**
- * Returns a response for a failing request containing an unique request ID. Details of the error are logged through
- * given logger.
- */
- public static ErrorResponse logThrowing(HttpRequest request, Logger logger, Throwable t) {
- String requestId = UUID.randomUUID().toString();
- logger.log(Level.SEVERE, "Unexpected error handling '" + request.getUri() + "' (request ID: " +
- requestId + ")", t);
- return ErrorResponse.internalServerError("Unexpected error occurred (request ID: " + requestId + ")");
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
deleted file mode 100644
index ebcc81ab756..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiHandler.java
+++ /dev/null
@@ -1,3479 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import ai.vespa.hosted.api.Signatures;
-import ai.vespa.http.DomainName;
-import ai.vespa.http.HttpURL;
-import ai.vespa.http.HttpURL.Query;
-import ai.vespa.validation.Validation;
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.google.common.base.Joiner;
-import com.google.common.collect.ImmutableSet;
-import com.yahoo.component.Version;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.ClusterResources;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.IntRange;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.handler.metrics.JsonResponse;
-import com.yahoo.container.jdisc.EmptyResponse;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.io.IOUtils;
-import com.yahoo.jdisc.http.filter.security.misc.User;
-import com.yahoo.restapi.ByteArrayResponse;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.MessageResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.ResourceResponse;
-import com.yahoo.restapi.RestApiException;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.JsonParseException;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-import com.yahoo.vespa.athenz.client.zms.ZmsClientException;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.NotExistsException;
-import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.SearchNodeMetrics;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.TenantId;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.TenantRoles;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.DeploymentResult;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.DeploymentResult.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Load;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneToken;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.FingerPrint;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
-import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.EndpointList;
-import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel;
-import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.Submission;
-import com.yahoo.vespa.hosted.controller.deployment.TestConfigSerializer;
-import com.yahoo.vespa.hosted.controller.maintenance.ResourceMeterMaintainer;
-import com.yahoo.vespa.hosted.controller.notification.Notification;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import com.yahoo.vespa.hosted.controller.persistence.SupportAccessSerializer;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.vespa.hosted.controller.restapi.dataplanetoken.DataplaneTokenService;
-import com.yahoo.vespa.hosted.controller.restapi.dataplanetoken.DataplaneTokenService.State;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationId;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationState;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationStatus;
-import com.yahoo.vespa.hosted.controller.security.AccessControlRequests;
-import com.yahoo.vespa.hosted.controller.security.Credentials;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccess;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Email;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-import com.yahoo.vespa.hosted.controller.tenant.PurchaseOrder;
-import com.yahoo.vespa.hosted.controller.tenant.TaxId;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.tenant.TenantAddress;
-import com.yahoo.vespa.hosted.controller.tenant.TenantBilling;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContact;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-import com.yahoo.vespa.hosted.controller.tenant.TermsOfServiceApproval;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.yolean.Exceptions;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.security.DigestInputStream;
-import java.security.Principal;
-import java.security.PublicKey;
-import java.time.DayOfWeek;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Base64;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.Scanner;
-import java.util.StringJoiner;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-import java.util.logging.Level;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.jdisc.Response.Status.BAD_REQUEST;
-import static com.yahoo.jdisc.Response.Status.CONFLICT;
-import static com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource.APPLICATION_TEST_ZIP;
-import static com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource.APPLICATION_ZIP;
-import static com.yahoo.yolean.Exceptions.uncheck;
-import static java.util.Comparator.comparingInt;
-import static java.util.Map.Entry.comparingByKey;
-import static java.util.stream.Collectors.collectingAndThen;
-import static java.util.stream.Collectors.joining;
-
-/**
- * This implements the application/v4 API which is used to deploy and manage applications
- * on hosted Vespa.
- *
- * @author bratseth
- * @author mpolden
- */
-@SuppressWarnings("unused") // created by injection
-public class ApplicationApiHandler extends AuditLoggingRequestHandler {
-
- private static final ObjectMapper jsonMapper = new ObjectMapper();
-
- private final Controller controller;
- private final AccessControlRequests accessControlRequests;
- private final TestConfigSerializer testConfigSerializer;
-
- @Inject
- public ApplicationApiHandler(ThreadedHttpRequestHandler.Context parentCtx,
- Controller controller,
- AccessControlRequests accessControlRequests) {
- super(parentCtx, controller.auditLogger());
- this.controller = controller;
- this.accessControlRequests = accessControlRequests;
- this.testConfigSerializer = new TestConfigSerializer(controller.system());
- }
-
- @Override
- public Duration getTimeout() {
- return Duration.ofMinutes(20); // deploys may take a long time;
- }
-
- @Override
- public HttpResponse auditAndHandle(HttpRequest request) {
- try {
- Path path = new Path(request.getUri());
- return switch (request.getMethod()) {
- case GET: yield handleGET(path, request);
- case PUT: yield handlePUT(path, request);
- case POST: yield handlePOST(path, request);
- case PATCH: yield handlePATCH(path, request);
- case DELETE: yield handleDELETE(path, request);
- case OPTIONS: yield handleOPTIONS();
- default: yield ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
- };
- }
- catch (RestApiException.Forbidden e) {
- return ErrorResponse.forbidden(Exceptions.toMessageString(e));
- }
- catch (RestApiException.Unauthorized e) {
- return ErrorResponse.unauthorized(Exceptions.toMessageString(e));
- }
- catch (NotExistsException e) {
- return ErrorResponse.notFoundError(Exceptions.toMessageString(e));
- }
- catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- }
- catch (ConfigServerException e) {
- return switch (e.code()) {
- case NOT_FOUND -> ErrorResponse.notFoundError(Exceptions.toMessageString(e));
- case ACTIVATION_CONFLICT -> new ErrorResponse(CONFLICT, e.code().name(), Exceptions.toMessageString(e));
- case INTERNAL_SERVER_ERROR -> ErrorResponses.logThrowing(request, log, e);
- default -> new ErrorResponse(BAD_REQUEST, e.code().name(), Exceptions.toMessageString(e));
- };
- }
- catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse handleGET(Path path, HttpRequest request) {
- if (path.matches("/application/v4/")) return root(request);
- if (path.matches("/application/v4/search/{*}")) return search(path, request);
- if (path.matches("/application/v4/notifications")) return notifications(request, Optional.ofNullable(request.getProperty("tenant")), true);
- if (path.matches("/application/v4/tenant")) return tenants(request);
- if (path.matches("/application/v4/tenant/{tenant}")) return tenant(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/access/request/operator")) return accessRequests(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/info")) return tenantInfo(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/info/profile")) return withCloudTenant(path.get("tenant"), this::tenantInfoProfile);
- if (path.matches("/application/v4/tenant/{tenant}/info/billing")) return withCloudTenant(path.get("tenant"), this::tenantInfoBilling);
- if (path.matches("/application/v4/tenant/{tenant}/info/contacts")) return withCloudTenant(path.get("tenant"), this::tenantInfoContacts);
- if (path.matches("/application/v4/tenant/{tenant}/notifications")) return notifications(request, Optional.of(path.get("tenant")), false);
- if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}/validate")) return validateSecretStore(path.get("tenant"), path.get("name"), request);
- if (path.matches("/application/v4/tenant/{tenant}/token")) return listTokens(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application")) return applications(path.get("tenant"), Optional.empty(), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return application(path.get("tenant"), path.get("application"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/compile-version")) return compileVersion(path.get("tenant"), path.get("application"), request.getProperty("allowMajor"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deployment")) return JobControllerApiHandlerHelper.overviewResponse(controller, TenantAndApplicationId.from(path.get("tenant"), path.get("application")), request.getUri());
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/package")) return applicationPackage(path.get("tenant"), path.get("application"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/diff/{number}")) return applicationPackageDiff(path.get("tenant"), path.get("application"), path.get("number"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return deploying(path.get("tenant"), path.get("application"), "default", request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance")) return applications(path.get("tenant"), Optional.of(path.get("application")), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return instance(path.get("tenant"), path.get("application"), path.get("instance"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying")) return deploying(path.get("tenant"), path.get("application"), path.get("instance"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job")) return JobControllerApiHandlerHelper.jobTypeResponse(controller, appIdFromPath(path), request.getUri());
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return JobControllerApiHandlerHelper.runResponse(controller, new JobId(appIdFromPath(path), jobTypeFromPath(path)), Optional.ofNullable(request.getProperty("limit")), request.getUri()); // (((\(✘෴✘)/)))
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/package")) return devApplicationPackage(appIdFromPath(path), jobTypeFromPath(path));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/diff/{number}")) return devApplicationPackageDiff(runIdFromPath(path));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/test-config")) return testConfig(appIdFromPath(path), jobTypeFromPath(path));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/run/{number}")) return JobControllerApiHandlerHelper.runDetailsResponse(controller.jobController(), runIdFromPath(path), request.getProperty("after"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/run/{number}/logs")) return JobControllerApiHandlerHelper.vespaLogsResponse(controller.jobController(), runIdFromPath(path), asLong(request.getProperty("from"), 0), request.getBooleanProperty("tester"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return deployment(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/reindexing")) return getReindexing(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/suspended")) return suspended(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/service/{service}/{host}/status/{*}")) return status(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.get("host"), path.getRest(), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/service/{service}/{host}/state/v1/{*}")) return stateV1(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.get("host"), path.getRest(), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/orchestrator")) return orchestrator(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/nodes")) return nodes(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/clusters")) return clusters(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/content/{*}")) return content(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.getRest(), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/logs")) return logs(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.propertyMap());
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/private-services")) return getPrivateServiceInfo(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/drop-documents")) return dropDocumentsStatus(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), Optional.ofNullable(request.getProperty("clusterId")).map(ClusterSpec.Id::from));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/access/support")) return supportAccess(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.propertyMap());
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/node/{node}/service-dump")) return getServiceDump(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("node"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/scaling")) return scaling(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- // TODO: Remove when not used anymore (migrated to ../metrics/searchnode)
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/metrics")) return searchNodeMetrics(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/metrics/searchnode")) return searchNodeMetrics(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation")) return rotationStatus(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), Optional.ofNullable(request.getProperty("endpointId")));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation/override")) return getGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deployment(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deployment(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/suspended")) return suspended(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/service/{service}/{host}/status/{*}")) return status(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("service"), path.get("host"), path.getRest(), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/nodes")) return nodes(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/clusters")) return clusters(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/logs")) return logs(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request.propertyMap());
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation")) return rotationStatus(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), Optional.ofNullable(request.getProperty("endpointId")));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation/override")) return getGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"));
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse search(Path path, HttpRequest request) {
- if (path.matches("/application/v4/search/deployment")) return searchDeploymentsByEndpoint(request);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse searchDeploymentsByEndpoint(HttpRequest request) {
- String endpoint = request.getProperty("endpoint");
- if (endpoint == null) {
- throw new IllegalArgumentException("Missing 'endpoint' query parameter");
- }
- endpoint = endpoint.trim();
- if (endpoint.startsWith("https://") || endpoint.startsWith("http://")) {
- // Trim scheme and port
- endpoint = URI.create(endpoint).getHost();
- }
- List<Application> applications = controller.applications().asList();
- record EndpointTarget(DeploymentId deployment, ClusterSpec.Id cluster) {}
- List<EndpointTarget> targets = new ArrayList<>();
- out:
- for (var app : applications) {
- Optional<Endpoint> declaredEndpoint = controller.routing().readDeclaredEndpointsOf(app).dnsName(endpoint);
- if (declaredEndpoint.isPresent()) {
- for (var target : declaredEndpoint.get().targets()) {
- targets.add(new EndpointTarget(target.deployment(), declaredEndpoint.get().cluster()));
- }
- break;
- } else {
- for (var instance : app.instances().values()) {
- for (var deployment : instance.deployments().values()) {
- DeploymentId id = new DeploymentId(instance.id(), deployment.zone());
- Optional<Endpoint> matchingEndpoint = controller.routing().readEndpointsOf(id).dnsName(endpoint);
- if (matchingEndpoint.isPresent()) {
- for (var target : matchingEndpoint.get().targets()) {
- targets.add(new EndpointTarget(target.deployment(), matchingEndpoint.get().cluster()));
- }
- break out;
- }
- }
- }
- }
- }
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor deploymentArray = root.setArray("deployments");
- for (var target : targets) {
- toSlime(target.deployment, target.cluster, deploymentArray.addObject(), request);
- }
- return new SlimeJsonResponse(slime);
- }
-
-
- private HttpResponse handlePUT(Path path, HttpRequest request) {
- if (path.matches("/application/v4/tenant/{tenant}")) return updateTenant(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/access/request/operator")) return requestSshAccess(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/access/approve/operator")) return approveAccessRequest(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/access/managed/operator")) return addManagedAccess(path.get("tenant"));
- if (path.matches("/application/v4/tenant/{tenant}/info")) return updateTenantInfo(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/info/profile")) return withCloudTenant(path.get("tenant"), request, this::putTenantInfoProfile);
- if (path.matches("/application/v4/tenant/{tenant}/info/billing")) return withCloudTenant(path.get("tenant"), request, this::putTenantInfoBilling);
- if (path.matches("/application/v4/tenant/{tenant}/info/contacts")) return withCloudTenant(path.get("tenant"), request, this::putTenantInfoContacts);
- if (path.matches("/application/v4/tenant/{tenant}/info/resend-mail-verification")) return withCloudTenant(path.get("tenant"), request, this::resendEmailVerification);
- if (path.matches("/application/v4/tenant/{tenant}/archive-access/aws")) return allowAwsArchiveAccess(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/archive-access/gcp")) return allowGcpArchiveAccess(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return addSecretStore(path.get("tenant"), path.get("name"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), false, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), false, request);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse handlePOST(Path path, HttpRequest request) {
- if (path.matches("/application/v4/tenant/{tenant}")) return createTenant(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/terms-of-service")) return approveTermsOfService(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/key")) return addDeveloperKey(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/token/{tokenid}")) return generateToken(path.get("tenant"), path.get("tokenid"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return createApplication(path.get("tenant"), path.get("application"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/platform")) return deployPlatform(path.get("tenant"), path.get("application"), "default", false, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/pin")) return deployPlatform(path.get("tenant"), path.get("application"), "default", true, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/platform-pin")) return deployPlatform(path.get("tenant"), path.get("application"), "default", true, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/application-pin")) return deployApplication(path.get("tenant"), path.get("application"), "default", true, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/application")) return deployApplication(path.get("tenant"), path.get("application"), "default", false, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/key")) return addDeployKey(path.get("tenant"), path.get("application"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/submit")) return submit(path.get("tenant"), path.get("application"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return createInstance(path.get("tenant"), path.get("application"), path.get("instance"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploy/{jobtype}")) return jobDeploy(appIdFromPath(path), jobTypeFromPath(path), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/platform")) return deployPlatform(path.get("tenant"), path.get("application"), path.get("instance"), false, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/pin")) return deployPlatform(path.get("tenant"), path.get("application"), path.get("instance"), true, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/platform-pin")) return deployPlatform(path.get("tenant"), path.get("application"), path.get("instance"), true, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/application-pin")) return deployApplication(path.get("tenant"), path.get("application"), path.get("instance"), true, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/application")) return deployApplication(path.get("tenant"), path.get("application"), path.get("instance"), false, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/submit")) return submit(path.get("tenant"), path.get("application"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return trigger(appIdFromPath(path), jobTypeFromPath(path), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/pause")) return pause(appIdFromPath(path), jobTypeFromPath(path));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return deploySystemApplication(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/deploy")) return deploySystemApplication(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); // legacy synonym of the above
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/reindex")) return reindex(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/reindexing")) return enableReindexing(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/restart")) return restart(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/suspend")) return suspend(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), true);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/drop-documents")) return dropDocuments(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), Optional.ofNullable(request.getProperty("clusterId")).map(ClusterSpec.Id::from));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/access/support")) return allowSupportAccess(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/node/{node}/service-dump")) return requestServiceDump(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), path.get("node"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deploySystemApplication(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/deploy")) return deploySystemApplication(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request); // legacy synonym of the above
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/restart")) return restart(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse handlePATCH(Path path, HttpRequest request) {
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return patchApplication(path.get("tenant"), path.get("application"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return patchApplication(path.get("tenant"), path.get("application"), request);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse handleDELETE(Path path, HttpRequest request) {
- if (path.matches("/application/v4/tenant/{tenant}")) return deleteTenant(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/access/managed/operator")) return removeManagedAccess(path.get("tenant"));
- if (path.matches("/application/v4/tenant/{tenant}/key")) return removeDeveloperKey(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/archive-access/aws")) return removeAwsArchiveAccess(path.get("tenant"));
- if (path.matches("/application/v4/tenant/{tenant}/archive-access/gcp")) return removeGcpArchiveAccess(path.get("tenant"));
- if (path.matches("/application/v4/tenant/{tenant}/secret-store/{name}")) return deleteSecretStore(path.get("tenant"), path.get("name"), request);
- if (path.matches("/application/v4/tenant/{tenant}/terms-of-service")) return unapproveTermsOfService(path.get("tenant"), request);
- if (path.matches("/application/v4/tenant/{tenant}/token/{tokenid}")) return deleteToken(path.get("tenant"), path.get("tokenid"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}")) return deleteApplication(path.get("tenant"), path.get("application"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deployment")) return removeAllProdDeployments(path.get("tenant"), path.get("application"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), "default", "all");
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), "default", path.get("choice"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/key")) return removeDeployKey(path.get("tenant"), path.get("application"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/submit/{build}")) return cancelBuild(path.get("tenant"), path.get("application"), path.get("build"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}")) return deleteInstance(path.get("tenant"), path.get("application"), path.get("instance"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("instance"), "all");
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploying/{choice}")) return cancelDeploy(path.get("tenant"), path.get("application"), path.get("instance"), path.get("choice"));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}")) return JobControllerApiHandlerHelper.abortJobResponse(controller.jobController(), request, appIdFromPath(path), jobTypeFromPath(path));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{jobtype}/pause")) return resume(appIdFromPath(path), jobTypeFromPath(path));
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return deactivate(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/reindexing")) return disableReindexing(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/suspend")) return suspend(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), false);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), true, request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/access/support")) return disallowSupportAccess(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}")) return deactivate(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), request);
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/instance/{instance}/global-rotation/override")) return setGlobalRotationOverride(path.get("tenant"), path.get("application"), path.get("instance"), path.get("environment"), path.get("region"), true, request);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse handleOPTIONS() {
- // We implement this to avoid redirect loops on OPTIONS requests from browsers, but do not really bother
- // spelling out the methods supported at each path, which we should
- EmptyResponse response = new EmptyResponse();
- response.headers().put("Allow", "GET,PUT,POST,PATCH,DELETE,OPTIONS");
- return response;
- }
-
- private HttpResponse recursiveRoot(HttpRequest request) {
- Slime slime = new Slime();
- Cursor tenantArray = slime.setArray();
- List<Application> applications = controller.applications().asList();
- for (Tenant tenant : controller.tenants().asList(includeDeleted(request)))
- toSlime(tenantArray.addObject(),
- tenant,
- applications.stream().filter(app -> app.id().tenant().equals(tenant.name())).toList(),
- request);
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse root(HttpRequest request) {
- return recurseOverTenants(request)
- ? recursiveRoot(request)
- : new ResourceResponse(request, "tenant");
- }
-
- private HttpResponse tenants(HttpRequest request) {
- Slime slime = new Slime();
- Cursor response = slime.setArray();
- for (Tenant tenant : controller.tenants().asList(includeDeleted(request)))
- tenantInTenantsListToSlime(tenant, request.getUri(), response.addObject());
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse tenant(String tenantName, HttpRequest request) {
- return controller.tenants().get(TenantName.from(tenantName), includeDeleted(request))
- .map(tenant -> tenant(tenant, request))
- .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist"));
- }
-
- private HttpResponse tenant(Tenant tenant, HttpRequest request) {
- Slime slime = new Slime();
- toSlime(slime.setObject(), tenant, controller.applications().asList(tenant.name()), request);
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse accessRequests(String tenantName, HttpRequest request) {
- var tenant = TenantName.from(tenantName);
- if (controller.tenants().require(tenant).type() != Tenant.Type.cloud)
- return ErrorResponse.badRequest("Can only see access requests for cloud tenants");
-
- var accessControlService = controller.serviceRegistry().accessControlService();
- var slime = new Slime();
- var cursor = slime.setObject();
- try {
- var accessRoleInformation = accessControlService.getAccessRoleInformation(tenant);
- var managedAccess = accessControlService.getManagedAccess(tenant);
- cursor.setBool("managedAccess", managedAccess);
- accessRoleInformation.getPendingRequest()
- .ifPresent(membershipRequest -> {
- var requestCursor = cursor.setObject("pendingRequest");
- requestCursor.setString("requestTime", membershipRequest.getCreationTime());
- requestCursor.setString("reason", membershipRequest.getReason());
- });
- var auditLogCursor = cursor.setArray("auditLog");
- accessRoleInformation.getAuditLog()
- .forEach(auditLogEntry -> {
- var entryCursor = auditLogCursor.addObject();
- entryCursor.setString("created", auditLogEntry.getCreationTime());
- entryCursor.setString("approver", auditLogEntry.getApprover());
- entryCursor.setString("reason", auditLogEntry.getReason());
- entryCursor.setString("status", auditLogEntry.getAction());
- });
- }
- catch (ZmsClientException e) {
- if (e.getErrorCode() == 404) cursor.setBool("managedAccess", false);
- }
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse requestSshAccess(String tenantName, HttpRequest request) {
- if (!isOperator(request)) {
- return ErrorResponse.forbidden("Only operators are allowed to request ssh access");
- }
-
- if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
- return ErrorResponse.badRequest("Can only request access for cloud tenants");
-
- controller.serviceRegistry().accessControlService().requestSshAccess(TenantName.from(tenantName));
- return new MessageResponse("OK");
-
- }
-
- private HttpResponse approveAccessRequest(String tenantName, HttpRequest request) {
- var tenant = TenantName.from(tenantName);
-
- if (controller.tenants().require(tenant).type() != Tenant.Type.cloud)
- return ErrorResponse.badRequest("Can only see access requests for cloud tenants");
-
- var inspector = toSlime(request.getData()).get();
- var expiry = inspector.field("expiry").valid() ?
- Instant.ofEpochMilli(inspector.field("expiry").asLong()) :
- Instant.now().plus(1, ChronoUnit.DAYS);
- var approve = inspector.field("approve").asBool();
-
- controller.serviceRegistry().accessControlService().decideSshAccess(tenant, expiry, OAuthCredentials.fromAuth0RequestContext(request.getJDiscRequest().context()), approve);
- return new MessageResponse("OK");
- }
-
- private HttpResponse addManagedAccess(String tenantName) {
- return setManagedAccess(tenantName, true);
- }
-
- private HttpResponse removeManagedAccess(String tenantName) {
- return setManagedAccess(tenantName, false);
- }
-
- private HttpResponse setManagedAccess(String tenantName, boolean managedAccess) {
- var tenant = TenantName.from(tenantName);
-
- if (controller.tenants().require(tenant).type() != Tenant.Type.cloud)
- return ErrorResponse.badRequest("Can only set access privel for cloud tenants");
-
- try {
- controller.serviceRegistry().accessControlService().setManagedAccess(tenant, managedAccess);
- var slime = new Slime();
- slime.setObject().setBool("managedAccess", managedAccess);
- return new SlimeJsonResponse(slime);
- }
- catch (ZmsClientException e) {
- if (e.getErrorCode() == 404) return ErrorResponse.conflict("Configuration not yet ready, please try again in a few minutes");
- throw e;
- }
- }
-
- private HttpResponse tenantInfo(String tenantName, HttpRequest request) {
- return controller.tenants().get(TenantName.from(tenantName))
- .filter(tenant -> tenant.type() == Tenant.Type.cloud)
- .map(tenant -> tenantInfo(((CloudTenant)tenant).info(), request))
- .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this"));
- }
-
- private HttpResponse withCloudTenant(String tenantName, Function<CloudTenant, SlimeJsonResponse> handler) {
- return controller.tenants().get(TenantName.from(tenantName))
- .filter(tenant -> tenant.type() == Tenant.Type.cloud)
- .map(tenant -> handler.apply((CloudTenant) tenant))
- .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this"));
- }
-
- private SlimeJsonResponse tenantInfo(TenantInfo info, HttpRequest request) {
- Slime slime = new Slime();
- Cursor infoCursor = slime.setObject();
- if (!info.isEmpty()) {
- infoCursor.setString("name", info.name());
- infoCursor.setString("email", info.email());
- infoCursor.setString("website", info.website());
- infoCursor.setString("contactName", info.contact().name());
- infoCursor.setString("contactEmail", info.contact().email().getEmailAddress());
- infoCursor.setBool("contactEmailVerified", info.contact().email().isVerified());
- toSlime(info.address(), infoCursor);
- toSlime(info.billingContact(), infoCursor);
- toSlime(info.contacts(), infoCursor);
- }
-
- return new SlimeJsonResponse(slime);
- }
-
- private SlimeJsonResponse tenantInfoProfile(CloudTenant cloudTenant) {
- var slime = new Slime();
- var root = slime.setObject();
- var info = cloudTenant.info();
-
- if (!info.isEmpty()) {
- var contact = root.setObject("contact");
- contact.setString("name", info.contact().name());
- contact.setString("email", info.contact().email().getEmailAddress());
- contact.setBool("emailVerified", info.contact().email().isVerified());
-
- var tenant = root.setObject("tenant");
- tenant.setString("company", info.name());
- tenant.setString("website", info.website());
-
- toSlime(info.address(), root); // will create "address" on the parent
- }
-
- return new SlimeJsonResponse(slime);
- }
-
- private SlimeJsonResponse withCloudTenant(String tenantName, HttpRequest request, BiFunction<CloudTenant, Inspector, SlimeJsonResponse> handler) {
- return controller.tenants().get(tenantName)
- .map(tenant -> handler.apply((CloudTenant) tenant, toSlime(request.getData()).get()))
- .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this"));
- }
-
- private SlimeJsonResponse putTenantInfoProfile(CloudTenant cloudTenant, Inspector inspector) {
- var info = cloudTenant.info();
-
- var mergedEmail = optional("email", inspector.field("contact"))
- .filter(address -> !address.equals(info.contact().email().getEmailAddress()))
- .map(address -> {
- controller.mailVerifier().sendMailVerification(cloudTenant.name(), address, PendingMailVerification.MailType.TENANT_CONTACT);
- return new Email(address, false);
- })
- .orElse(info.contact().email());
-
- var mergedContact = TenantContact.empty()
- .withName(getString(inspector.field("contact").field("name"), info.contact().name()))
- .withEmail(mergedEmail);
-
- var mergedAddress = updateTenantInfoAddress(inspector.field("address"), info.address());
-
- var mergedInfo = info
- .withName(getString(inspector.field("tenant").field("company"), info.name()))
- .withWebsite(getString(inspector.field("tenant").field("website"), info.website()))
- .withContact(mergedContact)
- .withAddress(mergedAddress);
-
- validateMergedTenantInfo(mergedInfo);
-
- controller.tenants().lockOrThrow(cloudTenant.name(), LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withInfo(mergedInfo);
- controller.tenants().store(lockedTenant);
- });
-
- return new MessageResponse("Tenant info updated");
- }
-
- private SlimeJsonResponse tenantInfoBilling(CloudTenant cloudTenant) {
- var slime = new Slime();
- var root = slime.setObject();
- var info = cloudTenant.info();
-
- if (!info.isEmpty()) {
- var billingContact = info.billingContact();
-
- var contact = root.setObject("contact");
- contact.setString("name", billingContact.contact().name());
- contact.setString("email", billingContact.contact().email().getEmailAddress());
- contact.setBool("emailVerified", billingContact.contact().email().isVerified());
- contact.setString("phone", billingContact.contact().phone());
- var taxIdCursor = root.setObject("taxId");
- taxIdCursor.setString("country", billingContact.getTaxId().country().value());
- taxIdCursor.setString("type", billingContact.getTaxId().type().value());
- taxIdCursor.setString("code", billingContact.getTaxId().code().value());
- root.setString("purchaseOrder", billingContact.getPurchaseOrder().value());
- root.setString("invoiceEmail", billingContact.getInvoiceEmail().getEmailAddress());
- var tosApprovalCursor = root.setObject("tosApproval");
- var tosApproval = billingContact.getToSApproval();
- tosApprovalCursor.setString("at", !tosApproval.isEmpty() ? tosApproval.approvedAt().toString() : "");
- tosApprovalCursor.setString("by", !tosApproval.isEmpty() ? tosApproval.approvedBy().get().getName() : "");
-
- toSlime(billingContact.address(), root); // will create "address" on the parent
- }
-
- return new SlimeJsonResponse(slime);
- }
-
- private SlimeJsonResponse putTenantInfoBilling(CloudTenant cloudTenant, Inspector inspector) {
- var info = cloudTenant.info();
- var billing = info.billingContact();
- var contact = billing.contact();
- var address = billing.address();
-
- var mergedContact = updateBillingContact(inspector.field("contact"), cloudTenant.name(), contact);
- var mergedAddress = updateTenantInfoAddress(inspector.field("address"), billing.address());
- var mergedTaxId = updateAndValidateTaxId(inspector.field("taxId"), billing.getTaxId());
- var mergedPurchaseOrder = optional("purchaseOrder", inspector).map(PurchaseOrder::new).orElse(billing.getPurchaseOrder());
- var mergedInvoiceEmail = optional("invoiceEmail", inspector).map(mail -> new Email(mail, false)).orElse(billing.getInvoiceEmail());
-
- var mergedBilling = info.billingContact()
- .withContact(mergedContact)
- .withAddress(mergedAddress)
- .withTaxId(mergedTaxId)
- .withPurchaseOrder(mergedPurchaseOrder)
- .withInvoiceEmail(mergedInvoiceEmail);
-
- var mergedInfo = info.withBilling(mergedBilling);
-
- // Store changes
- controller.tenants().lockOrThrow(cloudTenant.name(), LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withInfo(mergedInfo);
- controller.tenants().store(lockedTenant);
- });
-
- return new MessageResponse("Tenant info updated");
- }
-
- private SlimeJsonResponse tenantInfoContacts(CloudTenant cloudTenant) {
- var slime = new Slime();
- var root = slime.setObject();
- toSlime(cloudTenant.info().contacts(), root);
- return new SlimeJsonResponse(slime);
- }
-
- private SlimeJsonResponse putTenantInfoContacts(CloudTenant cloudTenant, Inspector inspector) {
- var mergedInfo = cloudTenant.info()
- .withContacts(updateTenantInfoContacts(inspector.field("contacts"), cloudTenant.name(), cloudTenant.info().contacts()));
-
- // Store changes
- controller.tenants().lockOrThrow(cloudTenant.name(), LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withInfo(mergedInfo);
- controller.tenants().store(lockedTenant);
- });
-
- return new MessageResponse("Tenant info updated");
- }
-
- private void validateMergedTenantInfo(TenantInfo mergedInfo) {
- // Assert that we have a valid tenant info
- if (mergedInfo.contact().name().isBlank()) {
- throw new IllegalArgumentException("'contactName' cannot be empty");
- }
- if (mergedInfo.contact().email().getEmailAddress().isBlank()) {
- throw new IllegalArgumentException("'contactEmail' cannot be empty");
- }
- if (! mergedInfo.contact().email().getEmailAddress().contains("@")) {
- // email address validation is notoriously hard - we should probably just try to send a
- // verification email to this address. checking for @ is a simple best-effort.
- throw new IllegalArgumentException("'contactEmail' needs to be an email address");
- }
- if (! mergedInfo.website().isBlank()) {
- try {
- new URL(mergedInfo.website());
- } catch (MalformedURLException e) {
- throw new IllegalArgumentException("'website' needs to be a valid address");
- }
- }
- if (! mergedInfo.billingContact().getInvoiceEmail().isBlank()) {
- // TODO: Validate invoice email is set if collection method is INVOICE
- if (! mergedInfo.billingContact().getInvoiceEmail().getEmailAddress().contains("@"))
- throw new IllegalArgumentException("'Invoice email' needs to be an email address");
- }
- }
-
- private void toSlime(TenantAddress address, Cursor parentCursor) {
- if (address.isEmpty()) return;
-
- Cursor addressCursor = parentCursor.setObject("address");
- addressCursor.setString("addressLines", address.address());
- addressCursor.setString("postalCodeOrZip", address.code());
- addressCursor.setString("city", address.city());
- addressCursor.setString("stateRegionProvince", address.region());
- addressCursor.setString("country", address.country());
- }
-
- private void toSlime(TenantBilling billingContact, Cursor parentCursor) {
- if (billingContact.isEmpty()) return;
-
- Cursor billingCursor = parentCursor.setObject("billingContact");
- billingCursor.setString("name", billingContact.contact().name());
- billingCursor.setString("email", billingContact.contact().email().getEmailAddress());
- billingCursor.setBool("emailVerified", billingContact.contact().email().isVerified());
- billingCursor.setString("phone", billingContact.contact().phone());
- var taxIdCursor = billingCursor.setObject("taxId");
- taxIdCursor.setString("country", billingContact.getTaxId().country().value());
- taxIdCursor.setString("type", billingContact.getTaxId().type().value());
- taxIdCursor.setString("code", billingContact.getTaxId().code().value());
- billingCursor.setString("purchaseOrder", billingContact.getPurchaseOrder().value());
- billingCursor.setString("invoiceEmail", billingContact.getInvoiceEmail().getEmailAddress());
- toSlime(billingContact.address(), billingCursor);
- var tosApprovalCursor = billingCursor.setObject("tosApproval");
- var tosApproval = billingContact.getToSApproval();
- tosApprovalCursor.setString("at", !tosApproval.isEmpty() ? tosApproval.approvedAt().toString() : "");
- tosApprovalCursor.setString("by", !tosApproval.isEmpty() ? tosApproval.approvedBy().get().getName() : "");
- }
-
- private void toSlime(TenantContacts contacts, Cursor parentCursor) {
- Cursor contactsCursor = parentCursor.setArray("contacts");
- contacts.all().forEach(contact -> {
- Cursor contactCursor = contactsCursor.addObject();
- Cursor audiencesArray = contactCursor.setArray("audiences");
- contact.audiences().forEach(audience -> audiencesArray.addString(toAudience(audience)));
- switch (contact.type()) {
- case EMAIL:
- var email = (TenantContacts.EmailContact) contact;
- contactCursor.setString("email", email.email().getEmailAddress());
- contactCursor.setBool("emailVerified", email.email().isVerified());
- return;
- default:
- throw new IllegalArgumentException("Serialization for contact type not implemented: " + contact.type());
- }
- });
- }
-
- private static TenantContacts.Audience fromAudience(String value) {
- return switch (value) {
- case "tenant": yield TenantContacts.Audience.TENANT;
- case "notifications": yield TenantContacts.Audience.NOTIFICATIONS;
- default: throw new IllegalArgumentException("Unknown contact audience '" + value + "'.");
- };
- }
-
- private static String toAudience(TenantContacts.Audience audience) {
- return switch (audience) {
- case TENANT: yield "tenant";
- case NOTIFICATIONS: yield "notifications";
- };
- }
-
-
- private HttpResponse updateTenantInfo(String tenantName, HttpRequest request) {
- return controller.tenants().get(TenantName.from(tenantName))
- .filter(tenant -> tenant.type() == Tenant.Type.cloud)
- .map(tenant -> updateTenantInfo(((CloudTenant)tenant), request))
- .orElseGet(() -> ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist or does not support this"));
- }
-
- private String getString(Inspector field, String defaultVale) {
- var string = field.valid() ? field.asString().trim() : defaultVale;
- if (string.length() > 512) throw new IllegalArgumentException("Input value too long");
- return string;
- }
-
- private SlimeJsonResponse updateTenantInfo(CloudTenant tenant, HttpRequest request) {
- TenantInfo oldInfo = tenant.info();
-
- // Merge info from request with the existing info
- Inspector insp = toSlime(request.getData()).get();
-
- var mergedEmail = optional("contactEmail", insp)
- .filter(address -> !address.equals(oldInfo.contact().email().getEmailAddress()))
- .map(address -> {
- controller.mailVerifier().sendMailVerification(tenant.name(), address, PendingMailVerification.MailType.TENANT_CONTACT);
- return new Email(address, false);
- })
- .orElse(oldInfo.contact().email());
-
- TenantContact mergedContact = TenantContact.empty()
- .withName(getString(insp.field("contactName"), oldInfo.contact().name()))
- .withEmail(mergedEmail);
-
- TenantInfo mergedInfo = TenantInfo.empty()
- .withName(getString(insp.field("name"), oldInfo.name()))
- .withEmail(getString(insp.field("email"), oldInfo.email()))
- .withWebsite(getString(insp.field("website"), oldInfo.website()))
- .withContact(mergedContact)
- .withAddress(updateTenantInfoAddress(insp.field("address"), oldInfo.address()))
- .withBilling(updateTenantInfoBillingContact(insp.field("billingContact"), tenant.name(), oldInfo.billingContact()))
- .withContacts(updateTenantInfoContacts(insp.field("contacts"), tenant.name(), oldInfo.contacts()));
-
- validateMergedTenantInfo(mergedInfo);
-
- // Store changes
- controller.tenants().lockOrThrow(tenant.name(), LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withInfo(mergedInfo);
- controller.tenants().store(lockedTenant);
- });
-
- return new MessageResponse("Tenant info updated");
- }
-
- private TenantAddress updateTenantInfoAddress(Inspector insp, TenantAddress oldAddress) {
- if (!insp.valid()) return oldAddress;
- TenantAddress address = TenantAddress.empty()
- .withCountry(getString(insp.field("country"), oldAddress.country()))
- .withRegion(getString(insp.field("stateRegionProvince"), oldAddress.region()))
- .withCity(getString(insp.field("city"), oldAddress.city()))
- .withCode(getString(insp.field("postalCodeOrZip"), oldAddress.code()))
- .withAddress(getString(insp.field("addressLines"), oldAddress.address()));
-
- List<String> fields = List.of(address.address(),
- address.code(),
- address.country(),
- address.city(),
- address.region());
-
- if (fields.stream().allMatch(String::isBlank) || fields.stream().noneMatch(String::isBlank))
- return address;
-
- throw new IllegalArgumentException("All address fields must be set");
- }
-
- private TaxId updateAndValidateTaxId(Inspector insp, TaxId old) {
- if (!insp.valid()) return old;
- var taxId = new TaxId(
- getString(insp.field("country"), old.country().value()),
- getString(insp.field("type"), old.type().value()),
- getString(insp.field("code"), old.code().value()));
- controller.serviceRegistry().billingController().validateTaxId(taxId);
- return taxId;
- }
-
- private TenantContact updateBillingContact(Inspector insp, TenantName tenantName, TenantContact oldContact) {
- if (!insp.valid()) return oldContact;
-
- var mergedEmail = optional("email", insp)
- .filter(address -> !address.equals(oldContact.email().getEmailAddress()))
- .map(address -> {
- controller.mailVerifier().sendMailVerification(tenantName, address, PendingMailVerification.MailType.BILLING);
- return new Email(address, false);
- })
- .orElse(oldContact.email());
-
- return TenantContact.empty()
- .withName(getString(insp.field("name"), oldContact.name()))
- .withEmail(mergedEmail)
- .withPhone(getString(insp.field("phone"), oldContact.phone()));
- }
-
- private TenantBilling updateTenantInfoBillingContact(Inspector insp, TenantName tenantName, TenantBilling oldContact) {
- if (!insp.valid()) return oldContact;
-
- var purchaseOrder = optional("purchaseOrder", insp).map(PurchaseOrder::new).orElse(oldContact.getPurchaseOrder());
- var invoiceEmail = optional("invoiceEmail", insp).map(mail -> new Email(mail, false)).orElse(oldContact.getInvoiceEmail());
- return TenantBilling.empty()
- .withContact(updateBillingContact(insp, tenantName, oldContact.contact()))
- .withAddress(updateTenantInfoAddress(insp.field("address"), oldContact.address()))
- .withTaxId(updateAndValidateTaxId(insp.field("taxId"), oldContact.getTaxId()))
- .withPurchaseOrder(purchaseOrder)
- .withInvoiceEmail(invoiceEmail);
- }
-
- private TenantContacts updateTenantInfoContacts(Inspector insp, TenantName tenantName, TenantContacts oldContacts) {
- if (!insp.valid()) return oldContacts;
-
- List<TenantContacts.EmailContact> contacts = SlimeUtils.entriesStream(insp).map(inspector -> {
- String email = inspector.field("email").asString().trim();
- List<TenantContacts.Audience> audiences = SlimeUtils.entriesStream(inspector.field("audiences"))
- .map(audience -> fromAudience(audience.asString()))
- .toList();
-
- // If contact exists, update audience. Otherwise, create new unverified contact
- return oldContacts.ofType(TenantContacts.EmailContact.class)
- .stream()
- .filter(contact -> contact.email().getEmailAddress().equals(email))
- .findAny()
- .map(emailContact -> new TenantContacts.EmailContact(audiences, emailContact.email()))
- .orElseGet(() -> {
- controller.mailVerifier().sendMailVerification(tenantName, email, PendingMailVerification.MailType.NOTIFICATIONS);
- return new TenantContacts.EmailContact(audiences, new Email(email, false));
- });
- }).toList();
-
- return new TenantContacts(contacts);
- }
-
- private HttpResponse notifications(HttpRequest request, Optional<String> tenant, boolean includeTenantFieldInResponse) {
- boolean productionOnly = showOnlyProductionInstances(request);
- boolean excludeMessages = "true".equals(request.getProperty("excludeMessages"));
- Slime slime = new Slime();
- Cursor notificationsArray = slime.setObject().setArray("notifications");
-
- tenant.map(t -> Stream.of(TenantName.from(t)))
- .orElseGet(() -> controller.notificationsDb().listTenantsWithNotifications().stream())
- .flatMap(tenantName -> controller.notificationsDb().listNotifications(NotificationSource.from(tenantName), productionOnly).stream())
- .filter(notification ->
- propertyEquals(request, "application", ApplicationName::from, notification.source().application()) &&
- propertyEquals(request, "instance", InstanceName::from, notification.source().instance()) &&
- propertyEquals(request, "zone", ZoneId::from, notification.source().zoneId()) &&
- propertyEquals(request, "job", job -> JobType.fromJobName(job, controller.zoneRegistry()), notification.source().jobType()) &&
- propertyEquals(request, "type", Notification.Type::valueOf, Optional.of(notification.type())) &&
- propertyEquals(request, "level", Notification.Level::valueOf, Optional.of(notification.level())))
- .forEach(notification -> toSlime(notificationsArray.addObject(), notification, includeTenantFieldInResponse, excludeMessages));
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse listTokens(String tenant, HttpRequest request) {
- Slime slime = new Slime();
- Cursor tokensArray = slime.setObject().setArray("tokens");
- controller.dataplaneTokenService().listTokensWithState(TenantName.from(tenant)).forEach((token, states) -> {
- Cursor tokenObject = tokensArray.addObject();
- tokenObject.setString("id", token.tokenId().value());
- tokenObject.setLong("lastUpdatedMillis", token.lastUpdated().toEpochMilli());
- Cursor fingerprintsArray = tokenObject.setArray("versions");
- for (var tokenVersion : token.tokenVersions()) {
- Cursor fingerprintObject = fingerprintsArray.addObject();
- fingerprintObject.setString("fingerprint", tokenVersion.fingerPrint().value());
- fingerprintObject.setString("created", tokenVersion.creationTime().toString());
- fingerprintObject.setString("author", tokenVersion.author());
- fingerprintObject.setString("expiration", tokenVersion.expiration().map(Instant::toString).orElse("none"));
- String tokenState = tokenVersion.expiration().map(controller.clock().instant()::isAfter).orElse(false)
- ? "expired"
- : valueOf(states.get(tokenVersion.fingerPrint()));
- fingerprintObject.setString("state", tokenState);
- }
- states.forEach((print, state) -> {
- if (state != State.REVOKING) return;
- Cursor fingerprintObject = fingerprintsArray.addObject();
- fingerprintObject.setString("fingerprint", print.value());
- fingerprintObject.setString("state", valueOf(state));
- });
- });
- return new SlimeJsonResponse(slime);
- }
-
- private static String valueOf(DataplaneTokenService.State state) {
- return switch (state) {
- case UNUSED: yield "unused";
- case DEPLOYING: yield "deploying";
- case ACTIVE: yield "active";
- case REVOKING: yield "revoking";
- };
- }
-
-
- private HttpResponse generateToken(String tenant, String tokenid, HttpRequest request) {
- var expiration = resolveExpiration(request).orElse(null);
- DataplaneToken token = controller.dataplaneTokenService().generateToken(
- TenantName.from(tenant), TokenId.of(tokenid), expiration, request.getJDiscRequest().getUserPrincipal());
- Slime slime = new Slime();
- Cursor tokenObject = slime.setObject();
- tokenObject.setString("id", token.tokenId().value());
- tokenObject.setString("token", token.tokenValue());
- tokenObject.setString("fingerprint", token.fingerPrint().value());
- tokenObject.setString("expiration", token.expiration().map(Instant::toString).orElse("none"));
- return new SlimeJsonResponse(slime);
- }
-
- /**
- * Specify 'expiration=none' for no expiration, no parameter or 'expiration=default' for default TTL.
- * Use ISO-8601 format for timestamp or period,
- * e.g 'expiration=PT1H' for 1 hour, 'expiration=2021-01-01T12:00:00Z' for a specific time.
- */
- private Optional<Instant> resolveExpiration(HttpRequest r) {
- var expirationParam = r.getProperty("expiration");
- var now = controller.clock().instant();
- if (expirationParam == null || expirationParam.equals("default"))
- return Optional.of(now.plus(DataplaneTokenService.DEFAULT_TTL));
- if (expirationParam.equals("none")) return Optional.empty();
- return expirationParam.startsWith("P")
- ? Optional.of(now.plus(Duration.parse(expirationParam)))
- : Optional.of(Instant.parse(expirationParam));
- }
-
- private HttpResponse deleteToken(String tenant, String tokenid, HttpRequest request) {
- String fingerprint = Optional.ofNullable(request.getProperty("fingerprint")).orElseThrow(() -> new IllegalArgumentException("Cannot delete token without fingerprint"));
- controller.dataplaneTokenService().deleteToken(TenantName.from(tenant), TokenId.of(tokenid), FingerPrint.of(fingerprint));
- return new MessageResponse("Token version deleted");
- }
-
- private static <T> boolean propertyEquals(HttpRequest request, String property, Function<String, T> mapper, Optional<T> value) {
- return Optional.ofNullable(request.getProperty(property))
- .map(propertyValue -> value.isPresent() && mapper.apply(propertyValue).equals(value.get()))
- .orElse(true);
- }
-
- private static void toSlime(Cursor cursor, Notification notification, boolean includeTenantFieldInResponse, boolean excludeMessages) {
- cursor.setLong("at", notification.at().toEpochMilli());
- cursor.setString("level", notificationLevelAsString(notification.level()));
- cursor.setString("type", notificationTypeAsString(notification.type()));
- if (!excludeMessages) {
- cursor.setString("title", notification.title());
- Cursor messagesArray = cursor.setArray("messages");
- notification.messages().forEach(messagesArray::addString);
- }
-
- if (includeTenantFieldInResponse) cursor.setString("tenant", notification.source().tenant().value());
- notification.source().application().ifPresent(application -> cursor.setString("application", application.value()));
- notification.source().instance().ifPresent(instance -> cursor.setString("instance", instance.value()));
- notification.source().zoneId().ifPresent(zoneId -> {
- cursor.setString("environment", zoneId.environment().value());
- cursor.setString("region", zoneId.region().value());
- });
- notification.source().clusterId().ifPresent(clusterId -> cursor.setString("clusterId", clusterId.value()));
- notification.source().jobType().ifPresent(jobType -> cursor.setString("jobName", jobType.jobName()));
- notification.source().runNumber().ifPresent(runNumber -> cursor.setLong("runNumber", runNumber));
- }
-
- private static String notificationTypeAsString(Notification.Type type) {
- return switch (type) {
- case submission, applicationPackage: yield "applicationPackage";
- case testPackage: yield "testPackage";
- case deployment: yield "deployment";
- case feedBlock: yield "feedBlock";
- case reindex: yield "reindex";
- case account: yield "account";
- };
- }
-
- private static String notificationLevelAsString(Notification.Level level) {
- return switch (level) {
- case info: yield "info";
- case warning: yield "warning";
- case error: yield "error";
- };
- }
-
- private HttpResponse applications(String tenantName, Optional<String> applicationName, HttpRequest request) {
- TenantName tenant = TenantName.from(tenantName);
- getTenantOrThrow(tenantName);
-
- List<Application> applications = applicationName.isEmpty() ?
- controller.applications().asList(tenant) :
- controller.applications().getApplication(TenantAndApplicationId.from(tenantName, applicationName.get()))
- .map(List::of)
- .orElseThrow(() -> new NotExistsException("Application '" + applicationName.get() + "' does not exist"));
-
- Slime slime = new Slime();
- Cursor applicationArray = slime.setArray();
- for (Application application : applications) {
- Cursor applicationObject = applicationArray.addObject();
- applicationObject.setString("tenant", application.id().tenant().value());
- applicationObject.setString("application", application.id().application().value());
- applicationObject.setString("url", withPath("/application/v4" +
- "/tenant/" + application.id().tenant().value() +
- "/application/" + application.id().application().value(),
- request.getUri()).toString());
- Cursor instanceArray = applicationObject.setArray("instances");
- for (InstanceName instance : showOnlyProductionInstances(request) ? application.productionInstances().keySet()
- : application.instances().keySet()) {
- Cursor instanceObject = instanceArray.addObject();
- instanceObject.setString("instance", instance.value());
- instanceObject.setString("url", withPath("/application/v4" +
- "/tenant/" + application.id().tenant().value() +
- "/application/" + application.id().application().value() +
- "/instance/" + instance.value(),
- request.getUri()).toString());
- }
- }
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse devApplicationPackage(ApplicationId id, JobType type) {
- ZoneId zone = type.zone();
- RevisionId revision = controller.jobController().last(id, type).get().versions().targetRevision();
- return new ZipResponse(id.toFullString() + "." + zone.value() + ".zip",
- controller.applications().applicationStore().stream(new DeploymentId(id, zone), revision));
- }
-
- private HttpResponse devApplicationPackageDiff(RunId runId) {
- DeploymentId deploymentId = new DeploymentId(runId.application(), runId.job().type().zone());
- return controller.applications().applicationStore().getDevDiff(deploymentId, runId.number())
- .map(ByteArrayResponse::new)
- .orElseThrow(() -> new NotExistsException("No application package diff found for " + runId));
- }
-
- private HttpResponse applicationPackage(String tenantName, String applicationName, HttpRequest request) {
- TenantAndApplicationId tenantAndApplication = TenantAndApplicationId.from(tenantName, applicationName);
- final long build;
- String requestedBuild = request.getProperty("build");
- if (requestedBuild != null) {
- if (requestedBuild.equals("latestDeployed")) {
- build = controller.applications().requireApplication(tenantAndApplication).latestDeployedRevision()
- .map(RevisionId::number)
- .orElseThrow(() -> new NotExistsException("no application package has been deployed in production for " + tenantAndApplication));
- } else {
- try {
- build = Validation.requireAtLeast(Long.parseLong(request.getProperty("build")), "build number", 1L);
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("invalid value for request parameter 'build'", e);
- }
- }
- } else {
- build = controller.applications().requireApplication(tenantAndApplication).revisions().last()
- .map(version -> version.id().number())
- .orElseThrow(() -> new NotExistsException("no application package has been submitted for " + tenantAndApplication));
- }
- RevisionId revision = RevisionId.forProduction(build);
- boolean tests = request.getBooleanProperty("tests");
- String filename = tenantAndApplication + (tests ? "-tests" : "-build") + revision.number() + ".zip";
- InputStream applicationPackage = tests ?
- controller.applications().applicationStore().streamTester(tenantAndApplication.tenant(), tenantAndApplication.application(), revision) :
- controller.applications().applicationStore().stream(new DeploymentId(tenantAndApplication.defaultInstance(), ZoneId.defaultId()), revision);
- return new ZipResponse(filename, applicationPackage);
- }
-
- private HttpResponse applicationPackageDiff(String tenant, String application, String number) {
- TenantAndApplicationId tenantAndApplication = TenantAndApplicationId.from(tenant, application);
- return controller.applications().applicationStore().getDiff(tenantAndApplication.tenant(), tenantAndApplication.application(), Long.parseLong(number))
- .map(ByteArrayResponse::new)
- .orElseThrow(() -> new NotExistsException("No application package diff found for '" + tenantAndApplication + "' with build number " + number));
- }
-
- private HttpResponse application(String tenantName, String applicationName, HttpRequest request) {
- Slime slime = new Slime();
- toSlime(slime.setObject(), getApplication(tenantName, applicationName), request);
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse compileVersion(String tenantName, String applicationName, String allowMajorParam) {
- Slime slime = new Slime();
- OptionalInt allowMajor = OptionalInt.empty();
- if (allowMajorParam != null) {
- try {
- allowMajor = OptionalInt.of(Integer.parseInt(allowMajorParam));
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("Invalid major version '" + allowMajorParam + "'", e);
- }
- }
- Version compileVersion = controller.applications().compileVersion(TenantAndApplicationId.from(tenantName, applicationName), allowMajor);
- slime.setObject().setString("compileVersion", compileVersion.toFullString());
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse instance(String tenantName, String applicationName, String instanceName, HttpRequest request) {
- Slime slime = new Slime();
- toSlime(slime.setObject(), getInstance(tenantName, applicationName, instanceName),
- controller.jobController().deploymentStatus(getApplication(tenantName, applicationName)), request);
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse approveTermsOfService(String tenant, HttpRequest req) {
- if (controller.tenants().require(TenantName.from(tenant)).type() != Tenant.Type.cloud)
- throw new IllegalArgumentException("Tenant '" + tenant + "' is not a cloud tenant");
- var approvedBy = SimplePrincipal.of(req.getJDiscRequest().getUserPrincipal());
- var approvedAt = controller.clock().instant();
-
- controller.tenants().lockOrThrow(TenantName.from(tenant), LockedTenant.Cloud.class, t -> {
- var updatedTenant = t.withInfo(t.get().info().withBilling(t.get().info().billingContact().withToSApproval(
- new TermsOfServiceApproval(approvedAt, approvedBy))));
- controller.tenants().store(updatedTenant);
- });
- return new MessageResponse("Terms of service approved by %s".formatted(approvedBy.getName()));
- }
-
- private HttpResponse unapproveTermsOfService(String tenant, HttpRequest req) {
- if (controller.tenants().require(TenantName.from(tenant)).type() != Tenant.Type.cloud)
- throw new IllegalArgumentException("Tenant '" + tenant + "' is not a cloud tenant");
- controller.tenants().lockOrThrow(TenantName.from(tenant), LockedTenant.Cloud.class, t -> {
- var updatedTenant = t.withInfo(t.get().info().withBilling(t.get().info().billingContact().withToSApproval(
- TermsOfServiceApproval.empty())));
- controller.tenants().store(updatedTenant);
- });
- return new MessageResponse("Terms of service approval removed");
- }
-
- private HttpResponse addDeveloperKey(String tenantName, HttpRequest request) {
- if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
- throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
-
- Principal user = request.getJDiscRequest().getUserPrincipal();
- String pemDeveloperKey = toSlime(request.getData()).get().field("key").asString();
- PublicKey developerKey = KeyUtils.fromPemEncodedPublicKey(pemDeveloperKey);
- Slime root = new Slime();
- controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> {
- tenant = tenant.withDeveloperKey(developerKey, user);
- toSlime(root.setObject().setArray("keys"), tenant.get().developerKeys());
- controller.tenants().store(tenant);
- });
- return new SlimeJsonResponse(root);
- }
-
- private HttpResponse validateSecretStore(String tenantName, String secretStoreName, HttpRequest request) {
- var awsRegion = request.getProperty("aws-region");
- var parameterName = request.getProperty("parameter-name");
- var applicationId = ApplicationId.fromFullString(request.getProperty("application-id"));
- if (!applicationId.tenant().equals(TenantName.from(tenantName)))
- return ErrorResponse.badRequest("Invalid application id");
- var zoneId = requireZone(ZoneId.from(request.getProperty("zone")));
- var deploymentId = new DeploymentId(applicationId, zoneId);
-
- var tenant = controller.tenants().require(applicationId.tenant(), CloudTenant.class);
-
- var tenantSecretStore = tenant.tenantSecretStores()
- .stream()
- .filter(secretStore -> secretStore.getName().equals(secretStoreName))
- .findFirst();
-
- if (tenantSecretStore.isEmpty())
- return ErrorResponse.notFoundError("No secret store '" + secretStoreName + "' configured for tenant '" + tenantName + "'");
-
- var response = controller.serviceRegistry().configServer().validateSecretStore(deploymentId, tenantSecretStore.get(), awsRegion, parameterName);
- try {
- var responseRoot = new Slime();
- var responseCursor = responseRoot.setObject();
- responseCursor.setString("target", deploymentId.toString());
- var responseResultCursor = responseCursor.setObject("result");
- var responseSlime = SlimeUtils.jsonToSlime(response);
- SlimeUtils.copyObject(responseSlime.get(), responseResultCursor);
- return new SlimeJsonResponse(responseRoot);
- } catch (JsonParseException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse removeDeveloperKey(String tenantName, HttpRequest request) {
- if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
- throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
-
- String pemDeveloperKey = toSlime(request.getData()).get().field("key").asString();
- PublicKey developerKey = KeyUtils.fromPemEncodedPublicKey(pemDeveloperKey);
- Principal user = controller.tenants().require(TenantName.from(tenantName), CloudTenant.class).developerKeys().get(developerKey);
- Slime root = new Slime();
- controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant -> {
- tenant = tenant.withoutDeveloperKey(developerKey);
- toSlime(root.setObject().setArray("keys"), tenant.get().developerKeys());
- controller.tenants().store(tenant);
- });
- return new SlimeJsonResponse(root);
- }
-
- private void toSlime(Cursor keysArray, Map<PublicKey, ? extends Principal> keys) {
- keys.forEach((key, principal) -> {
- Cursor keyObject = keysArray.addObject();
- keyObject.setString("key", KeyUtils.toPem(key));
- keyObject.setString("user", principal.getName());
- });
- }
-
- private HttpResponse addDeployKey(String tenantName, String applicationName, HttpRequest request) {
- String pemDeployKey = toSlime(request.getData()).get().field("key").asString();
- PublicKey deployKey = KeyUtils.fromPemEncodedPublicKey(pemDeployKey);
- Slime root = new Slime();
- controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> {
- application = application.withDeployKey(deployKey);
- application.get().deployKeys().stream()
- .map(KeyUtils::toPem)
- .forEach(root.setObject().setArray("keys")::addString);
- controller.applications().store(application);
- });
- return new SlimeJsonResponse(root);
- }
-
- private HttpResponse removeDeployKey(String tenantName, String applicationName, HttpRequest request) {
- String pemDeployKey = toSlime(request.getData()).get().field("key").asString();
- PublicKey deployKey = KeyUtils.fromPemEncodedPublicKey(pemDeployKey);
- Slime root = new Slime();
- controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> {
- application = application.withoutDeployKey(deployKey);
- application.get().deployKeys().stream()
- .map(KeyUtils::toPem)
- .forEach(root.setObject().setArray("keys")::addString);
- controller.applications().store(application);
- });
- return new SlimeJsonResponse(root);
- }
-
- private HttpResponse addSecretStore(String tenantName, String name, HttpRequest request) {
- if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
- throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
-
- var data = toSlime(request.getData()).get();
- var awsId = mandatory("awsId", data).asString();
- var externalId = mandatory("externalId", data).asString();
- var role = mandatory("role", data).asString();
-
- var tenant = controller.tenants().require(TenantName.from(tenantName), CloudTenant.class);
- var tenantSecretStore = new TenantSecretStore(name, awsId, role);
-
- if (!tenantSecretStore.isValid()) {
- return ErrorResponse.badRequest("Secret store " + tenantSecretStore + " is invalid");
- }
- if (tenant.tenantSecretStores().contains(tenantSecretStore)) {
- return ErrorResponse.badRequest("Secret store " + tenantSecretStore + " is already configured");
- }
-
- controller.serviceRegistry().roleService().createTenantPolicy(TenantName.from(tenantName), name, awsId, role);
- controller.serviceRegistry().tenantSecretService().addSecretStore(tenant.name(), tenantSecretStore, externalId);
- // Store changes
- controller.tenants().lockOrThrow(tenant.name(), LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withSecretStore(tenantSecretStore);
- controller.tenants().store(lockedTenant);
- });
-
- tenant = controller.tenants().require(TenantName.from(tenantName), CloudTenant.class);
- var slime = new Slime();
- toSlime(slime.setObject(), tenant.tenantSecretStores());
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse deleteSecretStore(String tenantName, String name, HttpRequest request) {
- var tenant = controller.tenants().require(TenantName.from(tenantName), CloudTenant.class);
-
- var optionalSecretStore = tenant.tenantSecretStores().stream()
- .filter(secretStore -> secretStore.getName().equals(name))
- .findFirst();
-
- if (optionalSecretStore.isEmpty())
- return ErrorResponse.notFoundError("Could not delete secret store '" + name + "': Secret store not found");
-
- var tenantSecretStore = optionalSecretStore.get();
- controller.serviceRegistry().tenantSecretService().deleteSecretStore(tenant.name(), tenantSecretStore);
- controller.serviceRegistry().roleService().deleteTenantPolicy(tenant.name(), tenantSecretStore.getName(), tenantSecretStore.getRole());
- controller.tenants().lockOrThrow(tenant.name(), LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withoutSecretStore(tenantSecretStore);
- controller.tenants().store(lockedTenant);
- });
-
- tenant = controller.tenants().require(TenantName.from(tenantName), CloudTenant.class);
- var slime = new Slime();
- toSlime(slime.setObject(), tenant.tenantSecretStores());
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse allowAwsArchiveAccess(String tenantName, HttpRequest request) {
- if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
- throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
-
- var data = toSlime(request.getData()).get();
- var role = mandatory("role", data).asString();
-
- if (role.isBlank()) {
- return ErrorResponse.badRequest("AWS archive access role can't be whitespace only");
- }
-
- controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> {
- var access = lockedTenant.get().archiveAccess();
- lockedTenant = lockedTenant.withArchiveAccess(access.withAWSRole(role));
- controller.tenants().store(lockedTenant);
- });
-
- return new MessageResponse("AWS archive access role set to '" + role + "' for tenant " + tenantName + ".");
- }
-
- private HttpResponse removeAwsArchiveAccess(String tenantName) {
- if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
- throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
-
- controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> {
- var access = lockedTenant.get().archiveAccess();
- lockedTenant = lockedTenant.withArchiveAccess(access.removeAWSRole());
- controller.tenants().store(lockedTenant);
- });
-
- return new MessageResponse("AWS archive access role removed for tenant " + tenantName + ".");
- }
-
- private HttpResponse allowGcpArchiveAccess(String tenantName, HttpRequest request) {
- if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
- throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
-
- var data = toSlime(request.getData()).get();
- var member = mandatory("member", data).asString();
-
- if (member.isBlank()) {
- return ErrorResponse.badRequest("GCP archive access role can't be whitespace only");
- }
-
- controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> {
- var access = lockedTenant.get().archiveAccess();
- lockedTenant = lockedTenant.withArchiveAccess(access.withGCPMember(member));
- controller.tenants().store(lockedTenant);
- });
-
- return new MessageResponse("GCP archive access member set to '" + member + "' for tenant " + tenantName + ".");
- }
-
- private HttpResponse removeGcpArchiveAccess(String tenantName) {
- if (controller.tenants().require(TenantName.from(tenantName)).type() != Tenant.Type.cloud)
- throw new IllegalArgumentException("Tenant '" + tenantName + "' is not a cloud tenant");
-
- controller.tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, lockedTenant -> {
- var access = lockedTenant.get().archiveAccess();
- lockedTenant = lockedTenant.withArchiveAccess(access.removeGCPMember());
- controller.tenants().store(lockedTenant);
- });
-
- return new MessageResponse("GCP archive access member removed for tenant " + tenantName + ".");
- }
-
- private HttpResponse patchApplication(String tenantName, String applicationName, HttpRequest request) {
- Inspector requestObject = toSlime(request.getData()).get();
- StringJoiner messageBuilder = new StringJoiner("\n").setEmptyValue("No applicable changes.");
- controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(tenantName, applicationName), application -> {
- Inspector majorVersionField = requestObject.field("majorVersion");
- if (majorVersionField.valid()) {
- Integer majorVersion = majorVersionField.asLong() == 0 ? null : (int) majorVersionField.asLong();
- application = application.withMajorVersion(majorVersion);
- messageBuilder.add("Set major version to " + (majorVersion == null ? "empty" : majorVersion));
- }
-
- // TODO jonmv: Remove when clients are updated.
- Inspector pemDeployKeyField = requestObject.field("pemDeployKey");
- if (pemDeployKeyField.valid()) {
- String pemDeployKey = pemDeployKeyField.asString();
- PublicKey deployKey = KeyUtils.fromPemEncodedPublicKey(pemDeployKey);
- application = application.withDeployKey(deployKey);
- messageBuilder.add("Added deploy key " + pemDeployKey);
- }
-
- controller.applications().store(application);
- });
- return new MessageResponse(messageBuilder.toString());
- }
-
- private Application getApplication(String tenantName, String applicationName) {
- TenantAndApplicationId applicationId = TenantAndApplicationId.from(tenantName, applicationName);
- return controller.applications().getApplication(applicationId)
- .orElseThrow(() -> new NotExistsException(applicationId + " not found"));
- }
-
- private Instance getInstance(String tenantName, String applicationName, String instanceName) {
- ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, instanceName);
- return controller.applications().getInstance(applicationId)
- .orElseThrow(() -> new NotExistsException(applicationId + " not found"));
- }
-
- private HttpResponse nodes(String tenantName, String applicationName, String instanceName, String environment, String region) {
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- ZoneId zone = requireZone(environment, region);
- List<Node> nodes = controller.serviceRegistry().configServer().nodeRepository().list(zone, NodeFilter.all().applications(id));
-
- Slime slime = new Slime();
- Cursor nodesArray = slime.setObject().setArray("nodes");
- for (Node node : nodes) {
- Cursor nodeObject = nodesArray.addObject();
- nodeObject.setString("hostname", node.hostname().value());
- nodeObject.setString("state", valueOf(node.state()));
- node.reservedTo().ifPresent(tenant -> nodeObject.setString("reservedTo", tenant.value()));
- nodeObject.setString("orchestration", valueOf(node.serviceState()));
- nodeObject.setString("version", node.currentVersion().toString());
- node.flavor().ifPresent(flavor -> nodeObject.setString("flavor", flavor));
- toSlime(node.resources(), nodeObject);
- nodeObject.setString("clusterId", node.clusterId());
- nodeObject.setString("clusterType", valueOf(node.clusterType()));
- nodeObject.setBool("down", node.down());
- nodeObject.setBool("retired", node.retired() || node.wantToRetire());
- nodeObject.setBool("restarting", node.wantedRestartGeneration() > node.restartGeneration());
- nodeObject.setBool("rebooting", node.wantedRebootGeneration() > node.rebootGeneration());
- nodeObject.setString("group", node.group());
- nodeObject.setLong("index", node.index());
- }
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse clusters(String tenantName, String applicationName, String instanceName, String environment, String region) {
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- ZoneId zone = requireZone(environment, region);
- com.yahoo.vespa.hosted.controller.api.integration.configserver.Application application = controller.serviceRegistry().configServer().nodeRepository().getApplication(zone, id);
-
- Slime slime = new Slime();
- Cursor clustersObject = slime.setObject().setObject("clusters");
- for (Cluster cluster : application.clusters().values()) {
- Cursor clusterObject = clustersObject.setObject(cluster.id().value());
- clusterObject.setString("type", cluster.type().name());
- toSlime(cluster.min(), clusterObject.setObject("min"));
- toSlime(cluster.max(), clusterObject.setObject("max"));
- if ( ! cluster.groupSize().isEmpty())
- toSlime(cluster.groupSize(), clusterObject.setObject("groupSize"));
- toSlime(cluster.current(), clusterObject.setObject("current"));
- toSlime(cluster.target(), clusterObject.setObject("target"));
- toSlime(cluster.suggested(), clusterObject.setObject("suggested"));
- scalingEventsToSlime(cluster.scalingEvents(), clusterObject.setArray("scalingEvents"));
- clusterObject.setLong("scalingDuration", cluster.scalingDuration().toMillis());
- }
- return new SlimeJsonResponse(slime);
- }
-
- private static String valueOf(Node.State state) {
- return switch (state) {
- case failed: yield "failed";
- case parked: yield "parked";
- case dirty: yield "dirty";
- case ready: yield "ready";
- case active: yield "active";
- case inactive: yield "inactive";
- case reserved: yield "reserved";
- case provisioned: yield "provisioned";
- case breakfixed: yield "breakfixed";
- case deprovisioned: yield "deprovisioned";
- default: throw new IllegalArgumentException("Unexpected node state '" + state + "'.");
- };
- }
-
- static String valueOf(Node.ServiceState state) {
- switch (state) {
- case expectedUp: return "expectedUp";
- case allowedDown: return "allowedDown";
- case permanentlyDown: return "permanentlyDown";
- case unorchestrated: return "unorchestrated";
- case unknown: break;
- }
-
- return "unknown";
- }
-
- private static String valueOf(Node.ClusterType type) {
- return switch (type) {
- case admin: yield "admin";
- case content: yield "content";
- case container: yield "container";
- case combined: yield "combined";
- case unknown: throw new IllegalArgumentException("Unexpected node cluster type '" + type + "'.");
- };
- }
-
- private static String valueOf(NodeResources.DiskSpeed diskSpeed) {
- return switch (diskSpeed) {
- case fast : yield "fast";
- case slow : yield "slow";
- case any : yield "any";
- };
- }
-
- private static String valueOf(NodeResources.StorageType storageType) {
- return switch (storageType) {
- case remote : yield "remote";
- case local : yield "local";
- case any : yield "any";
- };
- }
-
- private static String valueOf(NodeResources.Architecture architecture) {
- return switch (architecture) {
- case x86_64 : yield "x86_64";
- case arm64 : yield "arm64";
- case any : yield "any";
- };
- }
-
- private HttpResponse logs(String tenantName, String applicationName, String instanceName, String environment, String region, Map<String, String> queryParameters) {
- ApplicationId application = ApplicationId.from(tenantName, applicationName, instanceName);
- ZoneId zone = requireZone(environment, region);
- DeploymentId deployment = new DeploymentId(application, zone);
- InputStream logStream = controller.serviceRegistry().configServer().getLogs(deployment, queryParameters);
- return new HttpResponse(200) {
- @Override
- public void render(OutputStream outputStream) throws IOException {
- try (logStream) {
- logStream.transferTo(outputStream);
- }
- }
- @Override
- public long maxPendingBytes() {
- return 1 << 26;
- }
- };
- }
-
- private HttpResponse supportAccess(String tenantName, String applicationName, String instanceName, String environment, String region, Map<String, String> queryParameters) {
- DeploymentId deployment = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region));
- SupportAccess supportAccess = controller.supportAccess().forDeployment(deployment);
- return new SlimeJsonResponse(SupportAccessSerializer.serializeCurrentState(supportAccess, controller.clock().instant()));
- }
-
- // TODO support access: only let tenants (not operators!) allow access
- // TODO support access: configurable period of access?
- private HttpResponse allowSupportAccess(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- DeploymentId deployment = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region));
- Principal principal = requireUserPrincipal(request);
- Instant now = controller.clock().instant();
- SupportAccess allowed = controller.supportAccess().allow(deployment, now.plus(7, ChronoUnit.DAYS), principal.getName());
- return new SlimeJsonResponse(SupportAccessSerializer.serializeCurrentState(allowed, now));
- }
-
- private HttpResponse disallowSupportAccess(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- DeploymentId deployment = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region));
- Principal principal = requireUserPrincipal(request);
- SupportAccess disallowed = controller.supportAccess().disallow(deployment, principal.getName());
- controller.applications().deploymentTrigger().reTriggerOrAddToQueue(deployment, "re-triggered to disallow support access, by " + request.getJDiscRequest().getUserPrincipal().getName());
- return new SlimeJsonResponse(SupportAccessSerializer.serializeCurrentState(disallowed, controller.clock().instant()));
- }
-
- private HttpResponse searchNodeMetrics(String tenantName, String applicationName, String instanceName, String environment, String region) {
- ApplicationId application = ApplicationId.from(tenantName, applicationName, instanceName);
- ZoneId zone = requireZone(environment, region);
- DeploymentId deployment = new DeploymentId(application, zone);
- List<SearchNodeMetrics> searchNodeMetrics = controller.serviceRegistry().configServer().getSearchNodeMetrics(deployment);
- return buildResponseFromSearchNodeMetrics(searchNodeMetrics);
- }
-
- private HttpResponse scaling(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- var from = Optional.ofNullable(request.getProperty("from"))
- .map(Long::valueOf)
- .map(Instant::ofEpochSecond)
- .orElse(Instant.EPOCH);
- var until = Optional.ofNullable(request.getProperty("until"))
- .map(Long::valueOf)
- .map(Instant::ofEpochSecond)
- .orElse(Instant.now(controller.clock()));
-
- var application = ApplicationId.from(tenantName, applicationName, instanceName);
- var zone = requireZone(environment, region);
- var deployment = new DeploymentId(application, zone);
- var events = controller.serviceRegistry().resourceDatabase().scalingEvents(from, until, deployment);
- var slime = new Slime();
- var root = slime.setObject();
- for (var entry : events.entrySet()) {
- var serviceRoot = root.setArray(entry.getKey().clusterId().value());
- scalingEventsToSlime(entry.getValue(), serviceRoot);
- }
- return new SlimeJsonResponse(slime);
- }
-
- private JsonResponse buildResponseFromSearchNodeMetrics(List<SearchNodeMetrics> searchnodeMetrics) {
- try {
- var jsonObject = jsonMapper.createObjectNode();
- var jsonArray = jsonMapper.createArrayNode();
- for (SearchNodeMetrics metrics : searchnodeMetrics) {
- jsonArray.add(metrics.toJson());
- }
- jsonObject.set("metrics", jsonArray);
- return new JsonResponse(200, jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonObject));
- } catch (JsonProcessingException e) {
- log.log(Level.WARNING, "Unable to build JsonResponse with Proton data: " + e.getMessage(), e);
- return new JsonResponse(500, "");
- }
- }
-
-
-
- private HttpResponse trigger(ApplicationId id, JobType type, HttpRequest request) {
- Inspector requestObject = toSlime(request.getData()).get();
- boolean requireTests = ! requestObject.field("skipTests").asBool();
- boolean reTrigger = requestObject.field("reTrigger").asBool();
- boolean upgradeRevision = ! requestObject.field("skipRevision").asBool();
- boolean upgradePlatform = ! requestObject.field("skipUpgrade").asBool();
- String triggered = reTrigger
- ? controller.applications().deploymentTrigger()
- .reTrigger(id, type, "re-triggered by " + request.getJDiscRequest().getUserPrincipal().getName()).type().jobName()
- : controller.applications().deploymentTrigger()
- .forceTrigger(id, type, "triggered by " + request.getJDiscRequest().getUserPrincipal().getName(), requireTests, upgradeRevision, upgradePlatform)
- .stream().map(job -> job.type().jobName()).collect(joining(", "));
- String suppressedUpgrades = ( ! upgradeRevision || ! upgradePlatform ? ", without " : "") +
- (upgradeRevision ? "" : "revision") +
- ( ! upgradeRevision && ! upgradePlatform ? " and " : "") +
- (upgradePlatform ? "" : "platform") +
- ( ! upgradeRevision || ! upgradePlatform ? " upgrade" : "");
- return new MessageResponse(triggered.isEmpty() ? "Job " + type.jobName() + " for " + id + " not triggered"
- : "Triggered " + triggered + " for " + id + suppressedUpgrades);
- }
-
- private HttpResponse pause(ApplicationId id, JobType type) {
- Instant until = controller.clock().instant().plus(DeploymentTrigger.maxPause);
- controller.applications().deploymentTrigger().pauseJob(id, type, until);
- return new MessageResponse(type.jobName() + " for " + id + " paused for " + DeploymentTrigger.maxPause);
- }
-
- private HttpResponse resume(ApplicationId id, JobType type) {
- controller.applications().deploymentTrigger().resumeJob(id, type);
- return new MessageResponse(type.jobName() + " for " + id + " resumed");
- }
-
- private SlimeJsonResponse resendEmailVerification(CloudTenant tenant, Inspector inspector) {
- var mail = mandatory("mail", inspector).asString();
- var type = mandatory("mailType", inspector).asString();
-
- var mailType = switch (type) {
- case "contact" -> PendingMailVerification.MailType.TENANT_CONTACT;
- case "notifications" -> PendingMailVerification.MailType.NOTIFICATIONS;
- case "billing" -> PendingMailVerification.MailType.BILLING;
- default -> throw new IllegalArgumentException("Unknown mail type " + type);
- };
-
- var pendingVerification = controller.mailVerifier().resendMailVerification(tenant.name(), mail, mailType);
- return pendingVerification.isPresent() ? new MessageResponse("Re-sent verification mail to " + mail) :
- ErrorResponse.notFoundError("No pending mail verification found for " + mail);
- }
-
- private void toSlime(Cursor object, Application application, HttpRequest request) {
- object.setString("tenant", application.id().tenant().value());
- object.setString("application", application.id().application().value());
- object.setString("deployments", withPath("/application/v4" +
- "/tenant/" + application.id().tenant().value() +
- "/application/" + application.id().application().value() +
- "/job/",
- request.getUri()).toString());
-
- DeploymentStatus status = controller.jobController().deploymentStatus(application);
- application.revisions().last().ifPresent(version -> JobControllerApiHandlerHelper.toSlime(object.setObject("latestVersion"), version));
-
- application.projectId().ifPresent(id -> object.setLong("projectId", id));
-
- // TODO jonmv: Remove this when users are updated.
- application.instances().values().stream().findFirst().ifPresent(instance -> {
- // Currently deploying change
- if ( ! instance.change().isEmpty())
- toSlime(object.setObject("deploying"), instance.change(), application);
-
- // Outstanding change
- if ( ! status.outstandingChange(instance.name()).isEmpty())
- toSlime(object.setObject("outstandingChange"), status.outstandingChange(instance.name()), application);
- });
-
- application.majorVersion().ifPresent(majorVersion -> object.setLong("majorVersion", majorVersion));
-
- Cursor instancesArray = object.setArray("instances");
- for (Instance instance : showOnlyProductionInstances(request) ? application.productionInstances().values()
- : application.instances().values())
- toSlime(instancesArray.addObject(), status, instance, application.deploymentSpec(), request);
-
- application.deployKeys().stream().map(KeyUtils::toPem).forEach(object.setArray("pemDeployKeys")::addString);
-
- // Metrics
- Cursor metricsObject = object.setObject("metrics");
- metricsObject.setDouble("queryServiceQuality", application.metrics().queryServiceQuality());
- metricsObject.setDouble("writeServiceQuality", application.metrics().writeServiceQuality());
-
- // Activity
- Cursor activity = object.setObject("activity");
- application.activity().lastQueried().ifPresent(instant -> activity.setLong("lastQueried", instant.toEpochMilli()));
- application.activity().lastWritten().ifPresent(instant -> activity.setLong("lastWritten", instant.toEpochMilli()));
- application.activity().lastQueriesPerSecond().ifPresent(value -> activity.setDouble("lastQueriesPerSecond", value));
- application.activity().lastWritesPerSecond().ifPresent(value -> activity.setDouble("lastWritesPerSecond", value));
-
- application.ownershipIssueId().ifPresent(issueId -> object.setString("ownershipIssueId", issueId.value()));
- application.issueOwner().ifPresent(owner -> object.setString("owner", owner.value()));
- application.deploymentIssueId().ifPresent(issueId -> object.setString("deploymentIssueId", issueId.value()));
- }
-
- // TODO: Eliminate duplicated code in this and toSlime(Cursor, Instance, DeploymentStatus, HttpRequest)
- private void toSlime(Cursor object, DeploymentStatus status, Instance instance, DeploymentSpec deploymentSpec, HttpRequest request) {
- object.setString("instance", instance.name().value());
-
- if (deploymentSpec.instance(instance.name()).isPresent()) {
- // Jobs ordered according to deployment spec
- Collection<JobStatus> jobStatus = status.instanceJobs(instance.name()).values();
-
- if ( ! instance.change().isEmpty())
- toSlime(object.setObject("deploying"), instance.change(), status.application());
-
- // Outstanding change
- if ( ! status.outstandingChange(instance.name()).isEmpty())
- toSlime(object.setObject("outstandingChange"), status.outstandingChange(instance.name()), status.application());
-
- // Change blockers
- Cursor changeBlockers = object.setArray("changeBlockers");
- deploymentSpec.instance(instance.name()).ifPresent(spec -> spec.changeBlocker().forEach(changeBlocker -> {
- Cursor changeBlockerObject = changeBlockers.addObject();
- changeBlockerObject.setBool("versions", changeBlocker.blocksVersions());
- changeBlockerObject.setBool("revisions", changeBlocker.blocksRevisions());
- changeBlockerObject.setString("timeZone", changeBlocker.window().zone().getId());
- Cursor days = changeBlockerObject.setArray("days");
- changeBlocker.window().days().stream().map(DayOfWeek::getValue).forEach(days::addLong);
- Cursor hours = changeBlockerObject.setArray("hours");
- changeBlocker.window().hours().forEach(hours::addLong);
- }));
- }
-
- // Rotation ID
- addRotationId(object, instance);
-
- // Deployments sorted according to deployment spec
- List<Deployment> deployments = deploymentSpec.instance(instance.name())
- .map(spec -> sortedDeployments(instance.deployments().values(), spec))
- .orElse(List.copyOf(instance.deployments().values()));
-
- Cursor deploymentsArray = object.setArray("deployments");
- for (Deployment deployment : deployments) {
- Cursor deploymentObject = deploymentsArray.addObject();
-
- // Rotation status for this deployment
- if (deployment.zone().environment() == Environment.prod && ! instance.rotations().isEmpty())
- toSlime(instance.rotations(), instance.rotationStatus(), deployment, deploymentObject);
-
- if (recurseOverDeployments(request)) // List full deployment information when recursive.
- toSlime(deploymentObject, new DeploymentId(instance.id(), deployment.zone()), deployment, request);
- else {
- deploymentObject.setString("environment", deployment.zone().environment().value());
- deploymentObject.setString("region", deployment.zone().region().value());
- addAvailabilityZone(deploymentObject, deployment.zone());
- deploymentObject.setString("url", withPath(request.getUri().getPath() +
- "/instance/" + instance.name().value() +
- "/environment/" + deployment.zone().environment().value() +
- "/region/" + deployment.zone().region().value(),
- request.getUri()).toString());
- }
- }
- }
-
- // TODO(mpolden): Remove once MultiRegionTest stops expecting this field
- private void addRotationId(Cursor object, Instance instance) {
- // Legacy field. Identifies the first assigned rotation, if any.
- instance.rotations().stream()
- .map(AssignedRotation::rotationId)
- .findFirst()
- .ifPresent(rotation -> object.setString("rotationId", rotation.asString()));
- }
-
- private void toSlime(Cursor object, Instance instance, DeploymentStatus status, HttpRequest request) {
- Application application = status.application();
- object.setString("tenant", instance.id().tenant().value());
- object.setString("application", instance.id().application().value());
- object.setString("instance", instance.id().instance().value());
- object.setString("deployments", withPath("/application/v4" +
- "/tenant/" + instance.id().tenant().value() +
- "/application/" + instance.id().application().value() +
- "/instance/" + instance.id().instance().value() + "/job/",
- request.getUri()).toString());
-
- application.revisions().last().ifPresent(version -> {
- version.sourceUrl().ifPresent(url -> object.setString("sourceUrl", url));
- version.commit().ifPresent(commit -> object.setString("commit", commit));
- });
-
- application.projectId().ifPresent(id -> object.setLong("projectId", id));
-
- if (application.deploymentSpec().instance(instance.name()).isPresent()) {
- // Jobs ordered according to deployment spec
- Collection<JobStatus> jobStatus = status.instanceJobs(instance.name()).values();
-
- if ( ! instance.change().isEmpty())
- toSlime(object.setObject("deploying"), instance.change(), application);
-
- // Outstanding change
- if ( ! status.outstandingChange(instance.name()).isEmpty())
- toSlime(object.setObject("outstandingChange"), status.outstandingChange(instance.name()), application);
-
- // Change blockers
- Cursor changeBlockers = object.setArray("changeBlockers");
- application.deploymentSpec().instance(instance.name()).ifPresent(spec -> spec.changeBlocker().forEach(changeBlocker -> {
- Cursor changeBlockerObject = changeBlockers.addObject();
- changeBlockerObject.setBool("versions", changeBlocker.blocksVersions());
- changeBlockerObject.setBool("revisions", changeBlocker.blocksRevisions());
- changeBlockerObject.setString("timeZone", changeBlocker.window().zone().getId());
- Cursor days = changeBlockerObject.setArray("days");
- changeBlocker.window().days().stream().map(DayOfWeek::getValue).forEach(days::addLong);
- Cursor hours = changeBlockerObject.setArray("hours");
- changeBlocker.window().hours().forEach(hours::addLong);
- }));
- }
-
- application.majorVersion().ifPresent(majorVersion -> object.setLong("majorVersion", majorVersion));
-
- // Rotation ID
- addRotationId(object, instance);
-
- // Deployments sorted according to deployment spec
- List<Deployment> deployments = application.deploymentSpec().instance(instance.name())
- .map(spec -> sortedDeployments(instance.deployments().values(), spec))
- .orElse(List.copyOf(instance.deployments().values()));
- Cursor instancesArray = object.setArray("instances");
- for (Deployment deployment : deployments) {
- Cursor deploymentObject = instancesArray.addObject();
-
- // Rotation status for this deployment
- if (deployment.zone().environment() == Environment.prod) {
- // 0 rotations: No fields written
- // 1 rotation : Write legacy field and endpointStatus field
- // >1 rotation : Write only endpointStatus field
- if (instance.rotations().size() == 1) {
- // TODO(mpolden): Stop writing this field once clients stop expecting it
- toSlime(instance.rotationStatus().of(instance.rotations().get(0).rotationId(), deployment),
- deploymentObject);
- }
- if ( ! recurseOverDeployments(request) && ! instance.rotations().isEmpty()) { // TODO jonmv: clean up when clients have converged.
- toSlime(instance.rotations(), instance.rotationStatus(), deployment, deploymentObject);
- }
-
- }
-
- if (recurseOverDeployments(request)) // List full deployment information when recursive.
- toSlime(deploymentObject, new DeploymentId(instance.id(), deployment.zone()), deployment, request);
- else {
- deploymentObject.setString("environment", deployment.zone().environment().value());
- deploymentObject.setString("region", deployment.zone().region().value());
- deploymentObject.setString("instance", instance.id().instance().value()); // pointless
- addAvailabilityZone(deploymentObject, deployment.zone());
- deploymentObject.setString("url", withPath(request.getUri().getPath() +
- "/environment/" + deployment.zone().environment().value() +
- "/region/" + deployment.zone().region().value(),
- request.getUri()).toString());
- }
- }
- // Add dummy values for not-yet-existent prod deployments, and running dev/perf deployments.
- Stream.concat(status.jobSteps().keySet().stream()
- .filter(job -> job.application().instance().equals(instance.name()))
- .filter(job -> job.type().isProduction() && job.type().isDeployment()),
- controller.jobController().active(instance.id()).stream()
- .map(run -> run.id().job())
- .filter(job -> job.type().environment().isManuallyDeployed()))
- .map(job -> job.type().zone())
- .filter(zone -> ! instance.deployments().containsKey(zone))
- .forEach(zone -> {
- Cursor deploymentObject = instancesArray.addObject();
- deploymentObject.setString("environment", zone.environment().value());
- deploymentObject.setString("region", zone.region().value());
- });
-
-
- // TODO jonmv: Remove when clients are updated
- application.deployKeys().stream().findFirst().ifPresent(key -> object.setString("pemDeployKey", KeyUtils.toPem(key)));
-
- application.deployKeys().stream().map(KeyUtils::toPem).forEach(object.setArray("pemDeployKeys")::addString);
-
- // Metrics
- Cursor metricsObject = object.setObject("metrics");
- metricsObject.setDouble("queryServiceQuality", application.metrics().queryServiceQuality());
- metricsObject.setDouble("writeServiceQuality", application.metrics().writeServiceQuality());
-
- // Activity
- Cursor activity = object.setObject("activity");
- application.activity().lastQueried().ifPresent(instant -> activity.setLong("lastQueried", instant.toEpochMilli()));
- application.activity().lastWritten().ifPresent(instant -> activity.setLong("lastWritten", instant.toEpochMilli()));
- application.activity().lastQueriesPerSecond().ifPresent(value -> activity.setDouble("lastQueriesPerSecond", value));
- application.activity().lastWritesPerSecond().ifPresent(value -> activity.setDouble("lastWritesPerSecond", value));
-
- application.ownershipIssueId().ifPresent(issueId -> object.setString("ownershipIssueId", issueId.value()));
- application.issueOwner().ifPresent(owner -> object.setString("owner", owner.value()));
- application.deploymentIssueId().ifPresent(issueId -> object.setString("deploymentIssueId", issueId.value()));
- }
-
- private HttpResponse deployment(String tenantName, String applicationName, String instanceName, String environment,
- String region, HttpRequest request) {
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- Instance instance = controller.applications().getInstance(id)
- .orElseThrow(() -> new NotExistsException(id + " not found"));
-
- DeploymentId deploymentId = new DeploymentId(instance.id(),
- requireZone(environment, region));
-
- Deployment deployment = instance.deployments().get(deploymentId.zoneId());
- if (deployment == null)
- throw new NotExistsException(instance + " is not deployed in " + deploymentId.zoneId());
-
- Slime slime = new Slime();
- toSlime(slime.setObject(), deploymentId, deployment, request);
- return new SlimeJsonResponse(slime);
- }
-
- private void toSlime(Cursor object, Change change, Application application) {
- change.platform().ifPresent(version -> object.setString("version", version.toString()));
- change.revision().ifPresent(revision -> JobControllerApiHandlerHelper.toSlime(object.setObject("revision"), application.revisions().get(revision)));
- }
-
- private void toSlime(Endpoint endpoint, Cursor object) {
- if (endpoint.scope().multiDeployment()) {
- object.setString("id", endpoint.name());
- }
- object.setString("cluster", endpoint.cluster().value());
- object.setBool("tls", endpoint.tls());
- object.setString("url", endpoint.url().toString());
- object.setString("scope", endpointScopeString(endpoint.scope()));
- object.setString("routingMethod", routingMethodString(endpoint.routingMethod()));
- object.setBool("legacy", endpoint.legacy());
- switch (endpoint.authMethod()) {
- case mtls -> object.setString("authMethod", "mtls");
- case token -> object.setString("authMethod", "token");
- case none -> object.setString("authMethod", "none");
- }
- }
-
- private void toSlime(Cursor response, DeploymentId deploymentId, Deployment deployment, HttpRequest request) {
- response.setString("tenant", deploymentId.applicationId().tenant().value());
- response.setString("application", deploymentId.applicationId().application().value());
- response.setString("instance", deploymentId.applicationId().instance().value()); // pointless
- response.setString("environment", deploymentId.zoneId().environment().value());
- response.setString("region", deploymentId.zoneId().region().value());
- addAvailabilityZone(response, deployment.zone());
- var application = controller.applications().requireApplication(TenantAndApplicationId.from(deploymentId.applicationId()));
- boolean includeAllEndpoints = request.getBooleanProperty("includeAllEndpoints");
- boolean includeWeightedEndpoints = includeAllEndpoints || request.getBooleanProperty("includeWeightedEndpoints");
- boolean includeLegacyEndpoints = includeAllEndpoints || request.getBooleanProperty("includeLegacyEndpoints");
- var endpointArray = response.setArray("endpoints");
- for (var endpoint : endpointsOf(deploymentId, application, includeLegacyEndpoints, includeWeightedEndpoints)) {
- toSlime(endpoint, endpointArray.addObject());
- }
- response.setString("clusters", withPath(toPath(deploymentId) + "/clusters", request.getUri()).toString());
- response.setString("nodes", withPathAndQuery("/zone/v2/" + deploymentId.zoneId().environment() + "/" + deploymentId.zoneId().region() + "/nodes/v2/node/", "recursive=true&application=" + deploymentId.applicationId().tenant() + "." + deploymentId.applicationId().application() + "." + deploymentId.applicationId().instance(), request.getUri()).toString());
- response.setString("yamasUrl", monitoringSystemUri(deploymentId).toString());
- response.setString("version", deployment.version().toFullString());
- response.setString("revision", application.revisions().get(deployment.revision()).stringId()); // TODO jonmv or freva: ƪ(`▿▿▿▿´ƪ)
- response.setLong("build", deployment.revision().number());
- Instant lastDeploymentStart = controller.jobController().lastDeploymentStart(deploymentId.applicationId(), deployment);
- response.setLong("deployTimeEpochMs", lastDeploymentStart.toEpochMilli());
- controller.zoneRegistry().getDeploymentTimeToLive(deploymentId.zoneId())
- .ifPresent(deploymentTimeToLive -> response.setLong("expiryTimeEpochMs", lastDeploymentStart.plus(deploymentTimeToLive).toEpochMilli()));
-
- application.projectId().ifPresent(i -> response.setString("screwdriverId", String.valueOf(i)));
-
- if (controller.zoneRegistry().isExternal(deployment.cloudAccount())) {
- Cursor enclave = response.setObject("enclave");
- enclave.setString("cloudAccount", deployment.cloudAccount().value());
- controller.zoneRegistry().cloudAccountAthenzDomain(deployment.cloudAccount()).ifPresent(domain -> enclave.setString("athensDomain", domain.value()));
- }
-
- var instance = application.instances().get(deploymentId.applicationId().instance());
- if (instance != null) {
- if (!instance.rotations().isEmpty() && deployment.zone().environment() == Environment.prod)
- toSlime(instance.rotations(), instance.rotationStatus(), deployment, response);
-
- if (!deployment.zone().environment().isManuallyDeployed()) {
- DeploymentStatus status = controller.jobController().deploymentStatus(application);
- JobId jobId = new JobId(instance.id(), JobType.deploymentTo(deployment.zone()));
- Optional.ofNullable(status.jobSteps().get(jobId))
- .ifPresent(stepStatus -> {
- JobControllerApiHandlerHelper.toSlime(response.setObject("applicationVersion"), application.revisions().get(deployment.revision()));
- if ( ! status.jobsToRun().containsKey(stepStatus.job().get()))
- response.setString("status", "complete");
- else if ( ! stepStatus.readiness(instance.change()).okAt(controller.clock().instant()))
- response.setString("status", "pending");
- else
- response.setString("status", "running");
- });
- } else {
- var deploymentRun = controller.jobController().last(deploymentId.applicationId(), JobType.deploymentTo(deploymentId.zoneId()));
- deploymentRun.ifPresent(run -> {
- response.setString("status", run.hasEnded() ? "complete" : "running");
- });
- }
- }
-
- response.setDouble("quota", deployment.quota().rate());
- deployment.cost().ifPresent(cost -> response.setDouble("cost", cost));
-
- (controller.zoneRegistry().isExclave(deployment.cloudAccount()) ?
- controller.archiveBucketDb().archiveUriFor(deploymentId.zoneId(), deployment.cloudAccount(), false) :
- controller.archiveBucketDb().archiveUriFor(deploymentId.zoneId(), deploymentId.applicationId().tenant(), false))
- .ifPresent(archiveUri -> response.setString("archiveUri", archiveUri.toString()));
-
- Cursor activity = response.setObject("activity");
- deployment.activity().lastQueried().ifPresent(instant -> activity.setLong("lastQueried",
- instant.toEpochMilli()));
- deployment.activity().lastWritten().ifPresent(instant -> activity.setLong("lastWritten",
- instant.toEpochMilli()));
- deployment.activity().lastQueriesPerSecond().ifPresent(value -> activity.setDouble("lastQueriesPerSecond", value));
- deployment.activity().lastWritesPerSecond().ifPresent(value -> activity.setDouble("lastWritesPerSecond", value));
-
- // Metrics
- DeploymentMetrics metrics = deployment.metrics();
- Cursor metricsObject = response.setObject("metrics");
- metricsObject.setDouble("queriesPerSecond", metrics.queriesPerSecond());
- metricsObject.setDouble("writesPerSecond", metrics.writesPerSecond());
- metricsObject.setDouble("documentCount", metrics.documentCount());
- metricsObject.setDouble("queryLatencyMillis", metrics.queryLatencyMillis());
- metricsObject.setDouble("writeLatencyMillis", metrics.writeLatencyMillis());
- metrics.instant().ifPresent(instant -> metricsObject.setLong("lastUpdated", instant.toEpochMilli()));
- }
-
- private EndpointList endpointsOf(DeploymentId deploymentId, Application application, boolean includeLegacy, boolean includeWeighted) {
- EndpointList zoneEndpoints = controller.routing().readEndpointsOf(deploymentId).direct();
- EndpointList declaredEndpoints = controller.routing().readDeclaredEndpointsOf(application).targets(deploymentId);
- EndpointList endpoints = zoneEndpoints.and(declaredEndpoints);
- if (!includeLegacy) {
- endpoints = endpoints.not().legacy();
- }
- if (!includeWeighted) {
- endpoints = endpoints.not().scope(Endpoint.Scope.weighted);
- }
- return endpoints;
- }
-
- private void toSlime(RotationState state, Cursor object) {
- Cursor bcpStatus = object.setObject("bcpStatus");
- bcpStatus.setString("rotationStatus", rotationStateString(state));
- }
-
- private void toSlime(List<AssignedRotation> rotations, RotationStatus status, Deployment deployment, Cursor object) {
- var array = object.setArray("endpointStatus");
- for (var rotation : rotations) {
- var statusObject = array.addObject();
- var targets = status.of(rotation.rotationId());
- statusObject.setString("endpointId", rotation.endpointId().id());
- statusObject.setString("rotationId", rotation.rotationId().asString());
- statusObject.setString("clusterId", rotation.clusterId().value());
- statusObject.setString("status", rotationStateString(status.of(rotation.rotationId(), deployment)));
- statusObject.setLong("lastUpdated", targets.lastUpdated().toEpochMilli());
- }
- }
-
- private URI monitoringSystemUri(DeploymentId deploymentId) {
- return controller.zoneRegistry().getMonitoringSystemUri(deploymentId);
- }
-
- private HttpResponse setGlobalRotationOverride(String tenantName, String applicationName, String instanceName, String environment, String region, boolean inService, HttpRequest request) {
- Instance instance = controller.applications().requireInstance(ApplicationId.from(tenantName, applicationName, instanceName));
- ZoneId zone = requireZone(environment, region);
- Deployment deployment = instance.deployments().get(zone);
- if (deployment == null) {
- throw new NotExistsException(instance + " has no deployment in " + zone);
- }
- DeploymentId deploymentId = new DeploymentId(instance.id(), zone);
- RoutingStatus.Agent agent = isOperator(request) ? RoutingStatus.Agent.operator : RoutingStatus.Agent.tenant;
- RoutingStatus.Value status = inService ? RoutingStatus.Value.in : RoutingStatus.Value.out;
- controller.routing().of(deploymentId).setRoutingStatus(status, agent);
- return new MessageResponse(Text.format("Successfully set %s in %s %s service",
- instance.id().toShortString(), zone, inService ? "in" : "out of"));
- }
-
- private String serviceTypeIn(DeploymentId id) {
- CloudName cloud = controller.zoneRegistry().zones().all().get(id.zoneId()).get().getCloudName();
- if (CloudName.AWS.equals(cloud)) return "aws-private-link";
- if (CloudName.GCP.equals(cloud)) return "gcp-service-connect";
- return "unknown";
- }
-
- private HttpResponse getPrivateServiceInfo(String tenantName, String applicationName, String instanceName, String environment, String region) {
- DeploymentId id = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- ZoneId.from(environment, region));
- List<LoadBalancer> lbs = controller.serviceRegistry().configServer().getLoadBalancers(id.applicationId(), id.zoneId());
- Slime slime = new Slime();
- Cursor lbArray = slime.setObject().setArray("privateServices");
- for (LoadBalancer lb : lbs) {
- Cursor serviceObject = lbArray.addObject();
- serviceObject.setString("cluster", lb.cluster().value());
- lb.service().ifPresent(service -> {
- serviceObject.setString("serviceId", service.id()); // Really the "serviceName", but this is what the user needs >_<
- serviceObject.setString("type", serviceTypeIn(id));
- Cursor urnsArray = serviceObject.setArray("allowedUrns");
- for (AllowedUrn urn : service.allowedUrns()) {
- Cursor urnObject = urnsArray.addObject();
- urnObject.setString("type", switch (urn.type()) {
- case awsPrivateLink -> "aws-private-link";
- case gcpServiceConnect -> "gcp-service-connect";
- });
- urnObject.setString("urn", urn.urn());
- }
- Cursor endpointsArray = serviceObject.setArray("endpoints");
- controller.serviceRegistry().vpcEndpointService()
- .getConnections(new ClusterId(id, lb.cluster()), lb.cloudAccount())
- .forEach(endpoint -> {
- Cursor endpointObject = endpointsArray.addObject();
- endpointObject.setString("endpointId", endpoint.endpointId());
- endpointObject.setString("state", endpoint.stateValue().name());
- endpointObject.setString("detail", endpoint.stateString());
- });
- });
- }
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse dropDocumentsStatus(String tenant, String application, String instance, String environment, String region, Optional<ClusterSpec.Id> clusterId) {
- ZoneId zone = ZoneId.from(environment, region);
- if (!zone.environment().isManuallyDeployed())
- throw new IllegalArgumentException("Drop documents status is only available for manually deployed environments");
-
- ApplicationId applicationId = ApplicationId.from(tenant, application, instance);
- NodeFilter filters = NodeFilter.all()
- .states(Node.State.active)
- .applications(applicationId)
- .clusterTypes(Node.ClusterType.content, Node.ClusterType.combined);
- List<Node> nodes = controller.serviceRegistry().configServer().nodeRepository().list(zone, clusterId.map(filters::clusterIds).orElse(filters));
- if (nodes.isEmpty()) {
- throw new NotExistsException("No content nodes found for %s%s in %s".formatted(
- applicationId.toFullString(), clusterId.map(id -> " cluster " + id).orElse(""), zone));
- }
-
- Instant readiedAt = null;
- int numNoReport = 0, numInitial = 0, numDropped = 0, numReadied = 0, numStarted = 0;
- for (Node node : nodes) {
- Inspector report = Optional.ofNullable(node.reports().get("dropDocuments"))
- .map(json -> SlimeUtils.jsonToSlime(json).get()).orElse(null);
- if (report == null) numNoReport++;
- else if (report.field("startedAt").valid()) {
- numStarted++;
- readiedAt = SlimeUtils.instant(report.field("readiedAt"));
- } else if (report.field("readiedAt").valid()) numReadied++;
- else if (report.field("droppedAt").valid()) numDropped++;
- else numInitial++;
- }
-
- if (numInitial + numDropped > 0 && numNoReport + numReadied + numStarted > 0)
- return ErrorResponse.conflict("Last dropping of documents may have failed to clear all documents due " +
- "to concurrent topology changes, consider retrying");
-
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- if (numStarted + numNoReport == nodes.size()) {
- if (readiedAt != null) root.setLong("lastDropped", readiedAt.toEpochMilli());
- } else {
- Cursor progress = root.setObject("progress");
- progress.setLong("total", nodes.size());
- progress.setLong("dropped", numDropped + numReadied + numStarted);
- progress.setLong("started", numStarted + numNoReport);
- }
-
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse dropDocuments(String tenant, String application, String instance, String environment, String region, Optional<ClusterSpec.Id> clusterId) {
- ZoneId zone = ZoneId.from(environment, region);
- if (!zone.environment().isManuallyDeployed())
- throw new IllegalArgumentException("Drop documents status is only available for manually deployed environments");
-
- ApplicationId applicationId = ApplicationId.from(tenant, application, instance);
- controller.serviceRegistry().configServer().nodeRepository().dropDocuments(zone, applicationId, clusterId);
- return new MessageResponse("Triggered drop documents for " + applicationId.toFullString() +
- clusterId.map(id -> " and cluster " + id).orElse("") + " in " + zone);
- }
-
- private HttpResponse getGlobalRotationOverride(String tenantName, String applicationName, String instanceName, String environment, String region) {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- requireZone(environment, region));
- Slime slime = new Slime();
- Cursor array = slime.setObject().setArray("globalrotationoverride");
- Optional<Endpoint> primaryEndpoint = controller.routing().readDeclaredEndpointsOf(deploymentId.applicationId())
- .requiresRotation()
- .first();
- if (primaryEndpoint.isPresent()) {
- DeploymentRoutingContext context = controller.routing().of(deploymentId);
- RoutingStatus status = context.routingStatus();
- array.addString(primaryEndpoint.get().upstreamName(deploymentId));
- Cursor statusObject = array.addObject();
- statusObject.setString("status", status.value().name());
- statusObject.setString("reason", "");
- statusObject.setString("agent", status.agent().name());
- statusObject.setLong("timestamp", status.changedAt().getEpochSecond());
- }
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse rotationStatus(String tenantName, String applicationName, String instanceName, String environment, String region, Optional<String> endpointId) {
- ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, instanceName);
- Instance instance = controller.applications().requireInstance(applicationId);
- ZoneId zone = requireZone(environment, region);
- RotationId rotation = findRotationId(instance, endpointId);
- Deployment deployment = instance.deployments().get(zone);
- if (deployment == null) {
- throw new NotExistsException(instance + " has no deployment in " + zone);
- }
-
- Slime slime = new Slime();
- Cursor response = slime.setObject();
- toSlime(instance.rotationStatus().of(rotation, deployment), response);
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse deploying(String tenantName, String applicationName, String instanceName, HttpRequest request) {
- Instance instance = controller.applications().requireInstance(ApplicationId.from(tenantName, applicationName, instanceName));
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- if ( ! instance.change().isEmpty()) {
- instance.change().platform().ifPresent(version -> root.setString("platform", version.toString()));
- instance.change().revision().ifPresent(revision -> root.setString("application", revision.toString()));
- root.setBool("pinned", instance.change().isPlatformPinned());
- root.setBool("platform-pinned", instance.change().isPlatformPinned());
- root.setBool("application-pinned", instance.change().isRevisionPinned());
- }
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse suspended(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- requireZone(environment, region));
- boolean suspended = controller.applications().isSuspended(deploymentId);
- Slime slime = new Slime();
- Cursor response = slime.setObject();
- response.setBool("suspended", suspended);
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse status(String tenantName, String applicationName, String instanceName, String environment, String region, String serviceName, String host, HttpURL.Path restPath, HttpRequest request) {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region));
- return controller.serviceRegistry().configServer().getServiceNodePage(deploymentId,
- serviceName,
- DomainName.of(host),
- HttpURL.Path.parse("/status").append(restPath),
- Query.empty().add(request.getJDiscRequest().parameters()));
- }
-
- private HttpResponse orchestrator(String tenantName, String applicationName, String instanceName, String environment, String region) {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region));
- return controller.serviceRegistry().configServer().getServiceNodes(deploymentId);
- }
-
- private HttpResponse stateV1(String tenantName, String applicationName, String instanceName, String environment, String region, String serviceName, String host, HttpURL.Path rest, HttpRequest request) {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region));
- Query query = Query.empty().add(request.getJDiscRequest().parameters());
- query = query.set("forwarded-url", HttpURL.from(request.getUri()).withQuery(Query.empty()).asURI().toString());
- return controller.serviceRegistry().configServer().getServiceNodePage(
- deploymentId, serviceName, DomainName.of(host), HttpURL.Path.parse("/state/v1").append(rest), query);
- }
-
- private HttpResponse content(String tenantName, String applicationName, String instanceName, String environment, String region, HttpURL.Path restPath, HttpRequest request) {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName), requireZone(environment, region));
- return controller.serviceRegistry().configServer().getApplicationPackageContent(deploymentId, restPath, request.getUri());
- }
-
- private HttpResponse updateTenant(String tenantName, HttpRequest request) {
- getTenantOrThrow(tenantName);
- TenantName tenant = TenantName.from(tenantName);
- Inspector requestObject = toSlime(request.getData()).get();
- controller.tenants().update(accessControlRequests.specification(tenant, requestObject),
- accessControlRequests.credentials(tenant, requestObject, request.getJDiscRequest()));
- return tenant(controller.tenants().require(TenantName.from(tenantName)), request);
- }
-
- private HttpResponse createTenant(String tenantName, HttpRequest request) {
- TenantName tenant = TenantName.from(tenantName);
- Inspector requestObject = toSlime(request.getData()).get();
- controller.tenants().create(accessControlRequests.specification(tenant, requestObject),
- accessControlRequests.credentials(tenant, requestObject, request.getJDiscRequest()));
- if (controller.system().isPublic()) {
- User user = getAttribute(request, User.ATTRIBUTE_NAME, User.class);
- TenantInfo info = controller.tenants().require(tenant, CloudTenant.class)
- .info()
- .withContact(TenantContact.from(user.name(), new Email(user.email(), true)));
- // Store changes
- controller.tenants().lockOrThrow(tenant, LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withInfo(info);
- controller.tenants().store(lockedTenant);
- });
- }
- return tenant(controller.tenants().require(TenantName.from(tenantName)), request);
- }
-
- private HttpResponse createApplication(String tenantName, String applicationName, HttpRequest request) {
- Inspector requestObject = toSlime(request.getData()).get();
- TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName);
- Credentials credentials = accessControlRequests.credentials(id.tenant(), requestObject, request.getJDiscRequest());
- Application application = controller.applications().createApplication(id, credentials);
- Slime slime = new Slime();
- toSlime(id, slime.setObject(), request);
- return new SlimeJsonResponse(slime);
- }
-
- // TODO jonmv: Remove when clients are updated.
- private HttpResponse createInstance(String tenantName, String applicationName, String instanceName, HttpRequest request) {
- TenantAndApplicationId applicationId = TenantAndApplicationId.from(tenantName, applicationName);
- if (controller.applications().getApplication(applicationId).isEmpty())
- createApplication(tenantName, applicationName, request);
-
- controller.applications().createInstance(applicationId.instance(instanceName));
-
- Slime slime = new Slime();
- toSlime(applicationId.instance(instanceName), slime.setObject(), request);
- return new SlimeJsonResponse(slime);
- }
-
- /** Trigger deployment of the given Vespa version if a valid one is given, e.g., "7.8.9". */
- private HttpResponse deployPlatform(String tenantName, String applicationName, String instanceName, boolean pin, HttpRequest request) {
- String versionString = readToString(request.getData());
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- StringBuilder response = new StringBuilder();
- controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- Version version = Version.fromString(versionString);
- VersionStatus versionStatus = controller.readVersionStatus();
- if (version.equals(Version.emptyVersion))
- version = controller.systemVersion(versionStatus);
- if ( ! versionStatus.isActive(version) && ! isOperator(request))
- throw new IllegalArgumentException("Cannot trigger deployment of version '" + version + "': " +
- "Version is not active in this system. " +
- "Active versions: " + versionStatus.versions()
- .stream()
- .map(VespaVersion::versionNumber)
- .map(Version::toString)
- .collect(joining(", ")));
- Change change = Change.of(version);
- if (pin)
- change = change.withPlatformPin();
-
- controller.applications().deploymentTrigger().forceChange(id, change, isOperator(request));
- response.append("Triggered ").append(change).append(" for ").append(id);
- });
- return new MessageResponse(response.toString());
- }
-
- /** Trigger deployment to the last known application package for the given application. */
- private HttpResponse deployApplication(String tenantName, String applicationName, String instanceName, boolean pin, HttpRequest request) {
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- Inspector buildField = toSlime(request.getData()).get().field("build");
- long build = buildField.valid() ? buildField.asLong() : -1;
-
- StringBuilder response = new StringBuilder();
- controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- RevisionId revision = build == -1 ? application.get().revisions().last().get().id()
- : getRevision(application.get(), build);
- Change change = Change.of(revision);
- if (pin)
- change = change.withRevisionPin();
- controller.applications().deploymentTrigger().forceChange(id, change, isOperator(request));
- response.append("Triggered ").append(change).append(" for ").append(id);
- });
- return new MessageResponse(response.toString());
- }
-
- private RevisionId getRevision(Application application, long build) {
- return application.revisions().withPackage().stream()
- .map(ApplicationVersion::id)
- .filter(version -> version.number() == build)
- .findFirst()
- .filter(version -> controller.applications().applicationStore().hasBuild(application.id().tenant(),
- application.id().application(),
- build))
- .orElseThrow(() -> new IllegalArgumentException("Build number '" + build + "' was not found"));
- }
-
- private HttpResponse cancelBuild(String tenantName, String applicationName, String build){
- TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName);
- RevisionId revision = RevisionId.forProduction(Long.parseLong(build));
- controller.applications().lockApplicationOrThrow(id, application -> {
- controller.applications().store(application.withRevisions(revisions -> revisions.with(revisions.get(revision).skipped())));
- for (Instance instance : application.get().instances().values())
- if (instance.change().revision().equals(Optional.of(revision)))
- controller.applications().deploymentTrigger().cancelChange(instance.id(), ChangesToCancel.APPLICATION);
- });
- return new MessageResponse("Marked build '" + build + "' as non-deployable");
- }
-
- /** Cancel ongoing change for given application, e.g., everything with {"cancel":"all"} */
- private HttpResponse cancelDeploy(String tenantName, String applicationName, String instanceName, String choice) {
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- StringBuilder response = new StringBuilder();
- controller.applications().lockApplicationOrThrow(TenantAndApplicationId.from(id), application -> {
- Change change = application.get().require(id.instance()).change();
- if (change.isEmpty()) {
- response.append("No deployment in progress for ").append(id).append(" at this time");
- return;
- }
-
- ChangesToCancel cancel = ChangesToCancel.valueOf(choice.replaceAll("-", "_").toUpperCase());
- controller.applications().deploymentTrigger().cancelChange(id, cancel);
- response.append("Changed deployment from '").append(change).append("' to '").append(controller.applications().requireInstance(id).change()).append("' for ").append(id);
- });
-
- return new MessageResponse(response.toString());
- }
-
- /** Schedule reindexing of an application, or a subset of clusters, possibly on a subset of documents. */
- private HttpResponse reindex(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- ZoneId zone = requireZone(environment, region);
- List<String> clusterNames = Optional.ofNullable(request.getProperty("clusterId")).stream()
- .flatMap(clusters -> Stream.of(clusters.split(",")))
- .filter(cluster -> ! cluster.isBlank())
- .toList();
- List<String> documentTypes = Optional.ofNullable(request.getProperty("documentType")).stream()
- .flatMap(types -> Stream.of(types.split(",")))
- .filter(type -> ! type.isBlank())
- .toList();
-
- Double speed = request.hasProperty("speed") ? Double.parseDouble(request.getProperty("speed")) : null;
- boolean indexedOnly = request.getBooleanProperty("indexedOnly");
- controller.applications().reindex(id, zone, clusterNames, documentTypes, indexedOnly, speed, "reindexing triggered by " + requireUserPrincipal(request).getName());
- return new MessageResponse("Requested reindexing of " + id + " in " + zone +
- (clusterNames.isEmpty() ? "" : ", on clusters " + String.join(", ", clusterNames)) +
- (documentTypes.isEmpty() ? "" : ", for types " + String.join(", ", documentTypes)) +
- (indexedOnly ? ", for indexed types" : "") +
- (speed != null ? ", with speed " + speed : ""));
- }
-
- /** Gets reindexing status of an application in a zone. */
- private HttpResponse getReindexing(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- ZoneId zone = requireZone(environment, region);
- ApplicationReindexing reindexing = controller.applications().applicationReindexing(id, zone);
-
- Slime slime = new Slime();
- Cursor root = slime.setObject();
-
- root.setBool("enabled", reindexing.enabled());
-
- Cursor clustersArray = root.setArray("clusters");
- reindexing.clusters().entrySet().stream().sorted(comparingByKey())
- .forEach(cluster -> {
- Cursor clusterObject = clustersArray.addObject();
- clusterObject.setString("name", cluster.getKey());
-
- Cursor pendingArray = clusterObject.setArray("pending");
- cluster.getValue().pending().entrySet().stream().sorted(comparingByKey())
- .forEach(pending -> {
- Cursor pendingObject = pendingArray.addObject();
- pendingObject.setString("type", pending.getKey());
- pendingObject.setLong("requiredGeneration", pending.getValue());
- });
-
- Cursor readyArray = clusterObject.setArray("ready");
- cluster.getValue().ready().entrySet().stream().sorted(comparingByKey())
- .forEach(ready -> {
- Cursor readyObject = readyArray.addObject();
- readyObject.setString("type", ready.getKey());
- setStatus(readyObject, ready.getValue());
- });
- });
- return new SlimeJsonResponse(slime);
- }
-
- void setStatus(Cursor statusObject, ApplicationReindexing.Status status) {
- status.readyAt().ifPresent(readyAt -> statusObject.setLong("readyAtMillis", readyAt.toEpochMilli()));
- status.startedAt().ifPresent(startedAt -> statusObject.setLong("startedAtMillis", startedAt.toEpochMilli()));
- status.endedAt().ifPresent(endedAt -> statusObject.setLong("endedAtMillis", endedAt.toEpochMilli()));
- status.state().map(ApplicationApiHandler::toString).ifPresent(state -> statusObject.setString("state", state));
- status.message().ifPresent(message -> statusObject.setString("message", message));
- status.progress().ifPresent(progress -> statusObject.setDouble("progress", progress));
- status.speed().ifPresent(speed -> statusObject.setDouble("speed", speed));
- status.cause().ifPresent(cause -> statusObject.setString("cause", cause));
- }
-
- private static String toString(ApplicationReindexing.State state) {
- return switch (state) {
- case PENDING: yield "pending";
- case RUNNING: yield "running";
- case FAILED: yield "failed";
- case SUCCESSFUL: yield "successful";
- };
- }
-
- /** Enables reindexing of an application in a zone. */
- private HttpResponse enableReindexing(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- ZoneId zone = requireZone(environment, region);
- controller.applications().enableReindexing(id, zone);
- return new MessageResponse("Enabled reindexing of " + id + " in " + zone);
- }
-
- /** Disables reindexing of an application in a zone. */
- private HttpResponse disableReindexing(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- ApplicationId id = ApplicationId.from(tenantName, applicationName, instanceName);
- ZoneId zone = requireZone(environment, region);
- controller.applications().disableReindexing(id, zone);
- return new MessageResponse("Disabled reindexing of " + id + " in " + zone);
- }
-
- /** Schedule restart of deployment, or specific host in a deployment */
- private HttpResponse restart(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- requireZone(environment, region));
- RestartFilter restartFilter = new RestartFilter()
- .withHostName(Optional.ofNullable(request.getProperty("hostname")).map(HostName::of))
- .withClusterType(Optional.ofNullable(request.getProperty("clusterType")).map(ClusterSpec.Type::from))
- .withClusterId(Optional.ofNullable(request.getProperty("clusterId")).map(ClusterSpec.Id::from));
-
- controller.applications().restart(deploymentId, restartFilter);
- return new MessageResponse("Requested restart of " + deploymentId);
- }
-
- /** Set suspension status of the given deployment. */
- private HttpResponse suspend(String tenantName, String applicationName, String instanceName, String environment, String region, boolean suspend) {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- requireZone(environment, region));
- controller.applications().setSuspension(deploymentId, suspend);
- return new MessageResponse((suspend ? "Suspended" : "Resumed") + " orchestration of " + deploymentId);
- }
-
- private HttpResponse jobDeploy(ApplicationId id, JobType type, HttpRequest request) {
- if ( ! type.environment().isManuallyDeployed() && ! (isOperator(request) || controller.system().isCd()))
- throw new IllegalArgumentException("Direct deployments are only allowed to manually deployed environments.");
-
- controller.applications().verifyPlan(id.tenant());
-
- Map<String, byte[]> dataParts = parseDataParts(request);
- if ( ! dataParts.containsKey("applicationZip"))
- throw new IllegalArgumentException("Missing required form part 'applicationZip'");
-
- ApplicationPackage applicationPackage = new ApplicationPackage(dataParts.get(APPLICATION_ZIP));
- controller.applications().verifyApplicationIdentityConfiguration(id.tenant(),
- Optional.of(new DeploymentId(id, type.zone())),
- applicationPackage,
- Optional.of(requireUserPrincipal(request)));
-
- Optional<Version> version = Optional.ofNullable(dataParts.get("deployOptions"))
- .map(json -> SlimeUtils.jsonToSlime(json).get())
- .flatMap(options -> optional("vespaVersion", options))
- .map(Version::fromString);
-
- ensureApplicationExists(TenantAndApplicationId.from(id), request);
-
- boolean dryRun = Optional.ofNullable(dataParts.get("deployOptions"))
- .map(json -> SlimeUtils.jsonToSlime(json).get())
- .flatMap(options -> optional("dryRun", options))
- .map(Boolean::valueOf)
- .orElse(false);
-
- controller.jobController().deploy(id, type, version, applicationPackage, dryRun, isOperator(request));
- RunId runId = controller.jobController().last(id, type).get().id();
- Slime slime = new Slime();
- Cursor rootObject = slime.setObject();
- rootObject.setString("message", "Deployment started in " + runId +
- ". This may take about 15 minutes the first time.");
- rootObject.setLong("run", runId.number());
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse deploySystemApplication(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- ApplicationId applicationId = ApplicationId.from(tenantName, applicationName, instanceName);
- ZoneId zone = requireZone(environment, region);
-
- // Get deployOptions
- Map<String, byte[]> dataParts = parseDataParts(request);
- if ( ! dataParts.containsKey("deployOptions"))
- return ErrorResponse.badRequest("Missing required form part 'deployOptions'");
- Inspector deployOptions = SlimeUtils.jsonToSlime(dataParts.get("deployOptions")).get();
-
- // Resolve system application
- Optional<SystemApplication> systemApplication = SystemApplication.matching(applicationId);
- if (systemApplication.isEmpty() || ! systemApplication.get().hasApplicationPackage()) {
- return ErrorResponse.badRequest("Deployment of " + applicationId + " is not supported through this API");
- }
-
- // Make it explicit that version is not yet supported here
- String vespaVersion = deployOptions.field("vespaVersion").asString();
- if ( ! vespaVersion.isEmpty()) {
- return ErrorResponse.badRequest("Specifying version for " + applicationId + " is not permitted");
- }
-
- // To avoid second guessing the orchestrated upgrades of system applications we don't allow
- // deploying these during a system upgrade, i.e., when a new Vespa version is being rolled out
- VersionStatus versionStatus = controller.readVersionStatus();
- if (versionStatus.isUpgrading()) {
- throw new IllegalArgumentException("Deployment of system applications during a system upgrade is not allowed");
- }
- Optional<VespaVersion> systemVersion = versionStatus.systemVersion();
- if (systemVersion.isEmpty()) {
- throw new IllegalArgumentException("Deployment of system applications is not permitted until system version is determined");
- }
- DeploymentResult result = controller.applications()
- .deploySystemApplicationPackage(systemApplication.get(), zone, systemVersion.get().versionNumber());
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- root.setString("message", "Deployed " + systemApplication.get() + " in " + zone + " on " + systemVersion.get().versionNumber());
-
- Cursor logArray = root.setArray("prepareMessages");
- for (LogEntry logMessage : result.log()) {
- Cursor logObject = logArray.addObject();
- logObject.setLong("time", logMessage.epochMillis());
- logObject.setString("level", logMessage.level().getName());
- logObject.setString("message", logMessage.message());
- }
-
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse deleteTenant(String tenantName, HttpRequest request) {
- boolean forget = request.getBooleanProperty("forget");
- if (forget && ! isOperator(request))
- return ErrorResponse.forbidden("Only operators can forget a tenant");
-
- controller.tenants().delete(TenantName.from(tenantName),
- Optional.of(accessControlRequests.credentials(TenantName.from(tenantName),
- toSlime(request.getData()).get(),
- request.getJDiscRequest())),
- forget);
-
- return new MessageResponse("Deleted tenant " + tenantName);
- }
-
- private HttpResponse deleteApplication(String tenantName, String applicationName, HttpRequest request) {
- TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName);
- Credentials credentials = accessControlRequests.credentials(id.tenant(), toSlime(request.getData()).get(), request.getJDiscRequest());
- controller.applications().deleteApplication(id, credentials);
- return new MessageResponse("Deleted application " + id);
- }
-
- private HttpResponse deleteInstance(String tenantName, String applicationName, String instanceName, HttpRequest request) {
- TenantAndApplicationId id = TenantAndApplicationId.from(tenantName, applicationName);
- controller.applications().deleteInstance(id.instance(instanceName));
- if (controller.applications().requireApplication(id).instances().isEmpty()) {
- Credentials credentials = accessControlRequests.credentials(id.tenant(), toSlime(request.getData()).get(), request.getJDiscRequest());
- controller.applications().deleteApplication(id, credentials);
- }
- return new MessageResponse("Deleted instance " + id.instance(instanceName).toFullString());
- }
-
- private HttpResponse deactivate(String tenantName, String applicationName, String instanceName, String environment, String region, HttpRequest request) {
- DeploymentId id = new DeploymentId(ApplicationId.from(tenantName, applicationName, instanceName),
- requireZone(environment, region));
- // Attempt to deactivate application even if the deployment is not known by the controller
- controller.applications().deactivate(id.applicationId(), id.zoneId());
- controller.jobController().last(id.applicationId(), JobType.deploymentTo(id.zoneId()))
- .filter(run -> ! run.hasEnded())
- .ifPresent(last -> controller.jobController().abort(last.id(), "deployment deactivated by " + request.getJDiscRequest().getUserPrincipal().getName(), true));
- return new MessageResponse("Deactivated " + id);
- }
-
- /** Returns test config for indicated job, with production deployments of the default instance if the given is not in deployment spec. */
- private HttpResponse testConfig(ApplicationId id, JobType type) {
- Application application = controller.applications().requireApplication(TenantAndApplicationId.from(id));
- ApplicationId prodInstanceId = application.deploymentSpec().instance(id.instance()).isPresent()
- ? id : TenantAndApplicationId.from(id).defaultInstance();
- HashSet<DeploymentId> deployments = controller.applications()
- .getInstance(prodInstanceId).stream()
- .flatMap(instance -> instance.productionDeployments().keySet().stream())
- .map(zone -> new DeploymentId(prodInstanceId, zone))
- .collect(Collectors.toCollection(HashSet::new));
-
-
- // If a production job is specified, the production deployment of the orchestrated instance is the relevant one,
- // as user instances should not exist in prod.
- ApplicationId toTest = type.isProduction() ? prodInstanceId : id;
- if ( ! type.isProduction())
- deployments.add(new DeploymentId(toTest, type.zone()));
-
- Deployment deployment = application.require(toTest.instance()).deployments().get(type.zone());
- if (deployment == null)
- throw new NotExistsException(toTest + " is not deployed in " + type.zone());
-
- return new SlimeJsonResponse(testConfigSerializer.configSlime(id,
- type,
- false,
- deployment.version(),
- deployment.revision(),
- deployment.at(),
- controller.routing().readStepRunnerEndpointsOf(deployments),
- controller.applications().reachableContentClustersByZone(deployments)));
- }
-
- private HttpResponse requestServiceDump(String tenant, String application, String instance, String environment,
- String region, String hostname, HttpRequest request) {
- NodeRepository nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- ZoneId zone = requireZone(environment, region);
-
- // Check that no other service dump is in progress
- Slime report = getReport(nodeRepository, zone, tenant, application, instance, hostname).orElse(null);
- if (report != null) {
- Cursor cursor = report.get();
- // Note: same behaviour for both value '0' and missing value.
- boolean force = request.getBooleanProperty("force");
- if (!force && cursor.field("failedAt").asLong() == 0 && cursor.field("completedAt").asLong() == 0) {
- throw new IllegalArgumentException("Service dump already in progress for " + cursor.field("configId").asString());
- }
- }
- Slime requestPayload;
- try {
- requestPayload = SlimeUtils.jsonToSlimeOrThrow(request.getData().readAllBytes());
- } catch (Exception e) {
- throw new IllegalArgumentException("Missing or invalid JSON in request content", e);
- }
- Cursor requestPayloadCursor = requestPayload.get();
- String configId = requestPayloadCursor.field("configId").asString();
- long expiresAt = requestPayloadCursor.field("expiresAt").asLong();
- if (configId.isEmpty()) {
- throw new IllegalArgumentException("Missing configId");
- }
- Cursor artifactsCursor = requestPayloadCursor.field("artifacts");
- int artifactEntries = artifactsCursor.entries();
- if (artifactEntries == 0) {
- throw new IllegalArgumentException("Missing or empty 'artifacts'");
- }
-
- Slime dumpRequest = new Slime();
- Cursor dumpRequestCursor = dumpRequest.setObject();
- dumpRequestCursor.setLong("createdMillis", controller.clock().millis());
- dumpRequestCursor.setString("configId", configId);
- Cursor dumpRequestArtifactsCursor = dumpRequestCursor.setArray("artifacts");
- for (int i = 0; i < artifactEntries; i++) {
- dumpRequestArtifactsCursor.addString(artifactsCursor.entry(i).asString());
- }
- if (expiresAt > 0) {
- dumpRequestCursor.setLong("expiresAt", expiresAt);
- }
- Cursor dumpOptionsCursor = requestPayloadCursor.field("dumpOptions");
- if (dumpOptionsCursor.children() > 0) {
- SlimeUtils.copyObject(dumpOptionsCursor, dumpRequestCursor.setObject("dumpOptions"));
- }
- var reportsUpdate = Map.of("serviceDump", new String(uncheck(() -> SlimeUtils.toJsonBytes(dumpRequest))));
- nodeRepository.updateReports(zone, hostname, reportsUpdate);
- boolean wait = request.getBooleanProperty("wait");
- if (!wait) return new MessageResponse("Request created");
- return waitForServiceDumpResult(nodeRepository, zone, tenant, application, instance, hostname);
- }
-
- private HttpResponse getServiceDump(String tenant, String application, String instance, String environment,
- String region, String hostname, HttpRequest request) {
- NodeRepository nodeRepository = controller.serviceRegistry().configServer().nodeRepository();
- ZoneId zone = requireZone(environment, region);
- Slime report = getReport(nodeRepository, zone, tenant, application, instance, hostname)
- .orElseThrow(() -> new NotExistsException("No service dump for node " + hostname));
- return new SlimeJsonResponse(report);
- }
-
- private HttpResponse waitForServiceDumpResult(NodeRepository nodeRepository, ZoneId zone, String tenant,
- String application, String instance, String hostname) {
- int pollInterval = 2;
- Slime report;
- while (true) {
- report = getReport(nodeRepository, zone, tenant, application, instance, hostname).get();
- Cursor cursor = report.get();
- if (cursor.field("completedAt").asLong() > 0 || cursor.field("failedAt").asLong() > 0) {
- break;
- }
- final Slime copyForLambda = report;
- log.fine(() -> uncheck(() -> new String(SlimeUtils.toJsonBytes(copyForLambda))));
- log.fine("Sleeping " + pollInterval + " seconds before checking report status again");
- controller.sleeper().sleep(Duration.ofSeconds(pollInterval));
- }
- return new SlimeJsonResponse(report);
- }
-
- private Optional<Slime> getReport(NodeRepository nodeRepository, ZoneId zone, String tenant,
- String application, String instance, String hostname) {
- Node node;
- try {
- node = nodeRepository.getNode(zone, hostname);
- } catch (IllegalArgumentException e) {
- throw new NotExistsException(hostname);
- }
- ApplicationId app = ApplicationId.from(tenant, application, instance);
- ApplicationId owner = node.owner().orElseThrow(() -> new IllegalArgumentException("Node has no owner"));
- if (!app.equals(owner)) {
- throw new IllegalArgumentException("Node is not owned by " + app.toFullString());
- }
- String json = node.reports().get("serviceDump");
- if (json == null) return Optional.empty();
- return Optional.of(SlimeUtils.jsonToSlimeOrThrow(json));
- }
-
- private static SourceRevision toSourceRevision(Inspector object) {
- if (!object.field("repository").valid() ||
- !object.field("branch").valid() ||
- !object.field("commit").valid()) {
- throw new IllegalArgumentException("Must specify \"repository\", \"branch\", and \"commit\".");
- }
- return new SourceRevision(object.field("repository").asString(),
- object.field("branch").asString(),
- object.field("commit").asString());
- }
-
- private Tenant getTenantOrThrow(String tenantName) {
- return controller.tenants().get(tenantName)
- .orElseThrow(() -> new NotExistsException(new TenantId(tenantName)));
- }
-
- private void toSlime(Cursor object, Tenant tenant, List<Application> applications, HttpRequest request) {
- object.setString("tenant", tenant.name().value());
- object.setString("type", tenantType(tenant));
- switch (tenant.type()) {
- case athenz:
- AthenzTenant athenzTenant = (AthenzTenant) tenant;
- object.setString("athensDomain", athenzTenant.domain().getName());
- object.setString("property", athenzTenant.property().id());
- athenzTenant.propertyId().ifPresent(id -> object.setString("propertyId", id.toString()));
- athenzTenant.contact().ifPresent(c -> {
- object.setString("propertyUrl", c.propertyUrl().toString());
- object.setString("contactsUrl", c.url().toString());
- object.setString("issueCreationUrl", c.issueTrackerUrl().toString());
- Cursor contactsArray = object.setArray("contacts");
- c.persons().forEach(persons -> {
- Cursor personArray = contactsArray.addArray();
- persons.forEach(personArray::addString);
- });
- });
- break;
- case cloud: {
- CloudTenant cloudTenant = (CloudTenant) tenant;
-
- cloudTenant.creator().ifPresent(creator -> object.setString("creator", creator.getName()));
- Cursor pemDeveloperKeysArray = object.setArray("pemDeveloperKeys");
- cloudTenant.developerKeys().forEach((key, user) -> {
- Cursor keyObject = pemDeveloperKeysArray.addObject();
- keyObject.setString("key", KeyUtils.toPem(key));
- keyObject.setString("user", user.getName());
- });
-
- // TODO: remove this once console is updated
- toSlime(object, cloudTenant.tenantSecretStores());
-
- toSlime(object.setObject("integrations").setObject("aws"),
- controller.serviceRegistry().roleService().getTenantRole(tenant.name()),
- cloudTenant.tenantSecretStores());
-
- try {
- var usedQuota = applications.stream()
- .map(Application::quotaUsage)
- .reduce(QuotaUsage.none, QuotaUsage::add);
-
- toSlime(object.setObject("quota"), usedQuota);
- } catch (Exception e) {
- log.warning(String.format("Failed to get quota for tenant %s: %s", tenant.name(), Exceptions.toMessageString(e)));
- }
-
- toSlime(cloudTenant.archiveAccess(), object.setObject("archiveAccess"));
-
- break;
- }
- case deleted: break;
- default: throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'.");
- }
- // TODO jonmv: This should list applications, not instances.
- Cursor applicationArray = object.setArray("applications");
- for (Application application : applications) {
- DeploymentStatus status = null;
- Collection<Instance> instances = showOnlyProductionInstances(request) ? application.productionInstances().values()
- : application.instances().values();
-
- if (instances.isEmpty() && !showOnlyActiveInstances(request))
- toSlime(application.id(), applicationArray.addObject(), request);
-
- for (Instance instance : instances) {
- if (showOnlyActiveInstances(request) && instance.deployments().isEmpty())
- continue;
- if (recurseOverApplications(request)) {
- if (status == null) status = controller.jobController().deploymentStatus(application);
- toSlime(applicationArray.addObject(), instance, status, request);
- } else {
- toSlime(instance.id(), applicationArray.addObject(), request);
- }
- }
- }
- tenantMetaDataToSlime(tenant, applications, object.setObject("metaData"));
-
- if (!tenant.cloudAccounts().isEmpty()) {
- Cursor cloudAccounts = object.setArray("cloudAccounts");
- tenant.cloudAccounts().forEach(accountInfo -> {
- Cursor accountObject = cloudAccounts.addObject();
- accountObject.setString("cloudAccount", accountInfo.cloudAccount().value());
- accountObject.setString("templateVersion", accountInfo.templateVersion().toFullString());
- });
- }
- }
-
- private void toSlime(ArchiveAccess archiveAccess, Cursor object) {
- archiveAccess.awsRole().ifPresent(role -> object.setString("awsRole", role));
- archiveAccess.gcpMember().ifPresent(member -> object.setString("gcpMember", member));
- }
-
- private void toSlime(Cursor object, QuotaUsage usage) {
- object.setDouble("budgetUsed", usage.rate());
- }
-
- private void toSlime(ClusterResources resources, Cursor object) {
- object.setLong("nodes", resources.nodes());
- object.setLong("groups", resources.groups());
- toSlime(resources.nodeResources(), object.setObject("nodeResources"));
-
- double cost = ResourceMeterMaintainer.cost(resources, controller.serviceRegistry().zoneRegistry().system());
- object.setDouble("cost", cost);
- }
-
- private void toSlime(IntRange range, Cursor object) {
- range.from().ifPresent(from -> object.setLong("from", from));
- range.to().ifPresent(to -> object.setLong("to", to));
- }
-
- private void toSlime(Cluster.Autoscaling autoscaling, Cursor autoscalingObject) {
- autoscalingObject.setString("status", autoscaling.status());
- autoscalingObject.setString("description", autoscaling.description());
- autoscaling.resources().ifPresent(resources -> toSlime(resources, autoscalingObject.setObject("resources")));
- autoscalingObject.setLong("at", autoscaling.at().toEpochMilli());
- toSlime(autoscaling.peak(), autoscalingObject.setObject("peak"));
- toSlime(autoscaling.ideal(), autoscalingObject.setObject("ideal"));
- }
-
- private void toSlime(Load load, Cursor loadObject) {
- loadObject.setDouble("cpu", load.cpu());
- loadObject.setDouble("memory", load.memory());
- loadObject.setDouble("disk", load.disk());
- }
-
- private void scalingEventsToSlime(List<Cluster.ScalingEvent> scalingEvents, Cursor scalingEventsArray) {
- for (Cluster.ScalingEvent scalingEvent : scalingEvents) {
- Cursor scalingEventObject = scalingEventsArray.addObject();
- toSlime(scalingEvent.from(), scalingEventObject.setObject("from"));
- toSlime(scalingEvent.to(), scalingEventObject.setObject("to"));
- scalingEventObject.setLong("at", scalingEvent.at().toEpochMilli());
- scalingEvent.completion().ifPresent(completion -> scalingEventObject.setLong("completion", completion.toEpochMilli()));
- }
- }
-
- private void toSlime(NodeResources resources, Cursor object) {
- object.setDouble("vcpu", resources.vcpu());
- object.setDouble("memoryGb", resources.memoryGb());
- object.setDouble("diskGb", resources.diskGb());
- object.setDouble("bandwidthGbps", resources.bandwidthGbps());
- object.setString("diskSpeed", valueOf(resources.diskSpeed()));
- object.setString("storageType", valueOf(resources.storageType()));
- object.setString("architecture", valueOf(resources.architecture()));
- object.setLong("gpuCount", resources.gpuResources().count());
- object.setDouble("gpuMemoryGb", resources.gpuResources().memoryGb());
- }
-
- // A tenant has different content when in a list ... antipattern, but not solvable before application/v5
- private void tenantInTenantsListToSlime(Tenant tenant, URI requestURI, Cursor object) {
- object.setString("tenant", tenant.name().value());
- Cursor metaData = object.setObject("metaData");
- metaData.setString("type", tenantType(tenant));
- switch (tenant.type()) {
- case athenz:
- AthenzTenant athenzTenant = (AthenzTenant) tenant;
- metaData.setString("athensDomain", athenzTenant.domain().getName());
- metaData.setString("property", athenzTenant.property().id());
- break;
- case cloud: break;
- case deleted: break;
- default: throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'.");
- }
- object.setString("url", withPath("/application/v4/tenant/" + tenant.name().value(), requestURI).toString());
- }
-
- private void tenantMetaDataToSlime(Tenant tenant, List<Application> applications, Cursor object) {
- Optional<Instant> lastDev = applications.stream()
- .flatMap(application -> application.instances().values().stream())
- .flatMap(instance -> instance.deployments().values().stream()
- .filter(deployment -> deployment.zone().environment() == Environment.dev)
- .map(deployment -> controller.jobController().lastDeploymentStart(instance.id(), deployment)))
- .max(Comparator.naturalOrder())
- .or(() -> applications.stream()
- .flatMap(application -> application.instances().values().stream())
- .flatMap(instance -> JobType.allIn(controller.zoneRegistry()).stream()
- .filter(job -> job.environment() == Environment.dev)
- .flatMap(jobType -> controller.jobController().last(instance.id(), jobType).stream()))
- .map(Run::start)
- .max(Comparator.naturalOrder()));
- Optional<Instant> lastSubmission = applications.stream()
- .flatMap(app -> app.revisions().last().flatMap(ApplicationVersion::buildTime).stream())
- .max(Comparator.naturalOrder());
- object.setLong("createdAtMillis", tenant.createdAt().toEpochMilli());
- if (tenant.type() == Tenant.Type.deleted)
- object.setLong("deletedAtMillis", ((DeletedTenant) tenant).deletedAt().toEpochMilli());
- lastDev.ifPresent(instant -> object.setLong("lastDeploymentToDevMillis", instant.toEpochMilli()));
- lastSubmission.ifPresent(instant -> object.setLong("lastSubmissionToProdMillis", instant.toEpochMilli()));
-
- tenant.lastLoginInfo().get(LastLoginInfo.UserLevel.user)
- .ifPresent(instant -> object.setLong("lastLoginByUserMillis", instant.toEpochMilli()));
- tenant.lastLoginInfo().get(LastLoginInfo.UserLevel.developer)
- .ifPresent(instant -> object.setLong("lastLoginByDeveloperMillis", instant.toEpochMilli()));
- tenant.lastLoginInfo().get(LastLoginInfo.UserLevel.administrator)
- .ifPresent(instant -> object.setLong("lastLoginByAdministratorMillis", instant.toEpochMilli()));
- }
-
- /** Returns a copy of the given URI with the host and port from the given URI, the path set to the given path and the query set to given query*/
- private URI withPathAndQuery(String newPath, String newQuery, URI uri) {
- try {
- return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), newPath, newQuery, null);
- }
- catch (URISyntaxException e) {
- throw new RuntimeException("Will not happen", e);
- }
- }
-
- /** Returns a copy of the given URI with the host and port from the given URI and the path set to the given path */
- private URI withPath(String newPath, URI uri) {
- return withPathAndQuery(newPath, null, uri);
- }
-
- private String toPath(DeploymentId id) {
- return path("/application", "v4",
- "tenant", id.applicationId().tenant(),
- "application", id.applicationId().application(),
- "instance", id.applicationId().instance(),
- "environment", id.zoneId().environment(),
- "region", id.zoneId().region());
- }
-
- private long asLong(String valueOrNull, long defaultWhenNull) {
- if (valueOrNull == null) return defaultWhenNull;
- try {
- return Long.parseLong(valueOrNull);
- }
- catch (NumberFormatException e) {
- throw new IllegalArgumentException("Expected an integer but got '" + valueOrNull + "'");
- }
- }
-
- private Slime toSlime(InputStream jsonStream) {
- try {
- byte[] jsonBytes = IOUtils.readBytes(jsonStream, 1000 * 1000);
- return SlimeUtils.jsonToSlime(jsonBytes);
- } catch (IOException e) {
- throw new RuntimeException();
- }
- }
-
- private static Principal requireUserPrincipal(HttpRequest request) {
- Principal principal = request.getJDiscRequest().getUserPrincipal();
- if (principal == null) throw new IllegalArgumentException("Expected a user principal");
- return principal;
- }
-
- private Inspector mandatory(String key, Inspector object) {
- if ( ! object.field(key).valid())
- throw new IllegalArgumentException("'" + key + "' is missing");
- return object.field(key);
- }
-
- private Optional<String> optional(String key, Inspector object) {
- return SlimeUtils.optionalString(object.field(key));
- }
-
- private static String path(Object... elements) {
- return Joiner.on("/").join(elements);
- }
-
- private void toSlime(TenantAndApplicationId id, Cursor object, HttpRequest request) {
- object.setString("tenant", id.tenant().value());
- object.setString("application", id.application().value());
- object.setString("url", withPath("/application/v4" +
- "/tenant/" + id.tenant().value() +
- "/application/" + id.application().value(),
- request.getUri()).toString());
- }
-
- private void toSlime(ApplicationId id, Cursor object, HttpRequest request) {
- object.setString("tenant", id.tenant().value());
- object.setString("application", id.application().value());
- object.setString("instance", id.instance().value());
- object.setString("url", withPath("/application/v4" +
- "/tenant/" + id.tenant().value() +
- "/application/" + id.application().value() +
- "/instance/" + id.instance().value(),
- request.getUri()).toString());
- }
-
- private void toSlime(DeploymentId id, ClusterSpec.Id cluster, Cursor object, HttpRequest request) {
- object.setString("tenant", id.applicationId().tenant().value());
- object.setString("application", id.applicationId().application().value());
- object.setString("instance", id.applicationId().instance().value());
- object.setString("environment", id.zoneId().environment().value());
- object.setString("region", id.zoneId().region().value());
- object.setString("cluster", cluster.value());
- object.setString("url", withPath("/application/v4" +
- "/tenant/" + id.applicationId().tenant().value() +
- "/application/" + id.applicationId().application().value() +
- "/instance/" + id.applicationId().instance().value() +
- "/environment/" + id.zoneId().environment().value() +
- "/region/" + id.zoneId().region().value(),
- request.getUri()).toString());
- }
-
- private void stringsToSlime(List<String> strings, Cursor array) {
- for (String string : strings)
- array.addString(string);
- }
-
- private void toSlime(Cursor object, List<TenantSecretStore> tenantSecretStores) {
- Cursor secretStore = object.setArray("secretStores");
- tenantSecretStores.forEach(store -> {
- toSlime(secretStore.addObject(), store);
- });
- }
-
- private void toSlime(Cursor object, TenantRoles tenantRoles, List<TenantSecretStore> tenantSecretStores) {
- object.setString("tenantRole", tenantRoles.containerRole());
- var stores = object.setArray("accounts");
- tenantSecretStores.forEach(secretStore -> {
- toSlime(stores.addObject(), secretStore);
- });
- }
-
- private void toSlime(Cursor object, TenantSecretStore secretStore) {
- object.setString("name", secretStore.getName());
- object.setString("awsId", secretStore.getAwsId());
- object.setString("role", secretStore.getRole());
- }
-
- private String readToString(InputStream stream) {
- Scanner scanner = new Scanner(stream).useDelimiter("\\A");
- if ( ! scanner.hasNext()) return null;
- return scanner.next();
- }
-
- private static boolean recurseOverTenants(HttpRequest request) {
- return recurseOverApplications(request) || "tenant".equals(request.getProperty("recursive"));
- }
-
- private static boolean recurseOverApplications(HttpRequest request) {
- return recurseOverDeployments(request) || "application".equals(request.getProperty("recursive"));
- }
-
- private static boolean recurseOverDeployments(HttpRequest request) {
- return ImmutableSet.of("all", "true", "deployment").contains(request.getProperty("recursive"));
- }
-
- private static boolean showOnlyProductionInstances(HttpRequest request) {
- return "true".equals(request.getProperty("production"));
- }
-
- private static boolean showOnlyActiveInstances(HttpRequest request) {
- return "true".equals(request.getProperty("activeInstances"));
- }
-
- private static boolean includeDeleted(HttpRequest request) {
- return "true".equals(request.getProperty("includeDeleted"));
- }
-
- private static String tenantType(Tenant tenant) {
- return switch (tenant.type()) {
- case athenz: yield "ATHENS";
- case cloud: yield "CLOUD";
- case deleted: yield "DELETED";
- };
- }
-
- private static ApplicationId appIdFromPath(Path path) {
- return ApplicationId.from(path.get("tenant"), path.get("application"), path.get("instance"));
- }
-
- private JobType jobTypeFromPath(Path path) {
- return JobType.fromJobName(path.get("jobtype"), controller.zoneRegistry());
- }
-
- private RunId runIdFromPath(Path path) {
- long number = Long.parseLong(path.get("number"));
- return new RunId(appIdFromPath(path), jobTypeFromPath(path), number);
- }
-
- private HttpResponse submit(String tenant, String application, HttpRequest request) {
- TenantName tenantName = TenantName.from(tenant);
- controller.applications().verifyPlan(tenantName);
-
- Map<String, byte[]> dataParts = parseDataParts(request);
- Inspector submitOptions = SlimeUtils.jsonToSlime(dataParts.get(EnvironmentResource.SUBMIT_OPTIONS)).get();
- long projectId = submitOptions.field("projectId").asLong(); // Absence of this means it's not a prod app :/
- projectId = projectId == 0 ? 1 : projectId;
- Optional<String> repository = optional("repository", submitOptions);
- Optional<String> branch = optional("branch", submitOptions);
- Optional<String> commit = optional("commit", submitOptions);
- Optional<SourceRevision> sourceRevision = repository.isPresent() && branch.isPresent() && commit.isPresent()
- ? Optional.of(new SourceRevision(repository.get(), branch.get(), commit.get()))
- : Optional.empty();
- Optional<String> sourceUrl = optional("sourceUrl", submitOptions);
- Optional<String> authorEmail = optional("authorEmail", submitOptions);
- Optional<String> description = optional("description", submitOptions);
- int risk = (int) submitOptions.field("risk").asLong();
-
- sourceUrl.map(URI::create).ifPresent(url -> {
- if (url.getHost() == null || url.getScheme() == null)
- throw new IllegalArgumentException("Source URL must include scheme and host");
- });
-
- ApplicationPackage applicationPackage =
- new ApplicationPackage(dataParts.get(APPLICATION_ZIP), true, controller.system().isPublic());
- byte[] testPackage = dataParts.getOrDefault(APPLICATION_TEST_ZIP, new byte[0]);
- Submission submission = new Submission(applicationPackage, testPackage, sourceUrl, sourceRevision, authorEmail, description, controller.clock().instant(), risk);
-
- controller.applications().verifyApplicationIdentityConfiguration(tenantName,
- Optional.empty(),
- applicationPackage,
- Optional.of(requireUserPrincipal(request)));
-
- TenantAndApplicationId id = TenantAndApplicationId.from(tenant, application);
- ensureApplicationExists(id, request);
- return JobControllerApiHandlerHelper.submitResponse(controller.jobController(), id, submission, projectId);
- }
-
- private HttpResponse removeAllProdDeployments(String tenant, String application) {
- JobControllerApiHandlerHelper.submitResponse(controller.jobController(),
- TenantAndApplicationId.from(tenant, application),
- new Submission(ApplicationPackage.deploymentRemoval(), new byte[0], Optional.empty(),
- Optional.empty(), Optional.empty(), Optional.empty(), controller.clock().instant(), 0),
- 0);
- return new MessageResponse("All deployments removed");
- }
-
- private void addAvailabilityZone(Cursor object, ZoneId zoneId) {
- ZoneApi zone = controller.zoneRegistry().get(zoneId);
- if (!zone.getCloudName().equals(CloudName.AWS)) return;
- object.setString("availabilityZone", zone.getCloudNativeAvailabilityZone());
- }
-
- private ZoneId requireZone(String environment, String region) {
- return requireZone(ZoneId.from(environment, region));
- }
-
- private ZoneId requireZone(ZoneId zone) {
- // TODO(mpolden): Find a way to not hardcode this. Some APIs allow this "virtual" zone, e.g. /logs
- if (zone.environment() == Environment.prod && zone.region().value().equals("controller")) {
- return zone;
- }
- if (!controller.zoneRegistry().hasZone(zone)) {
- throw new IllegalArgumentException("Zone " + zone + " does not exist in this system");
- }
- return zone;
- }
-
- private static Map<String, byte[]> parseDataParts(HttpRequest request) {
- String contentHash = request.getHeader("X-Content-Hash");
- if (contentHash == null)
- return new MultipartParser().parse(request);
-
- DigestInputStream digester = Signatures.sha256Digester(request.getData());
- var dataParts = new MultipartParser().parse(request.getHeader("Content-Type"), digester, request.getUri());
- if ( ! Arrays.equals(digester.getMessageDigest().digest(), Base64.getDecoder().decode(contentHash)))
- throw new IllegalArgumentException("Value of X-Content-Hash header does not match computed content hash");
-
- return dataParts;
- }
-
- private static RotationId findRotationId(Instance instance, Optional<String> endpointId) {
- if (instance.rotations().isEmpty()) {
- throw new NotExistsException("global rotation does not exist for " + instance);
- }
- if (endpointId.isPresent()) {
- return instance.rotations().stream()
- .filter(r -> r.endpointId().id().equals(endpointId.get()))
- .map(AssignedRotation::rotationId)
- .findFirst()
- .orElseThrow(() -> new NotExistsException("endpoint " + endpointId.get() +
- " does not exist for " + instance));
- } else if (instance.rotations().size() > 1) {
- throw new IllegalArgumentException(instance + " has multiple rotations. Query parameter 'endpointId' must be given");
- }
- return instance.rotations().get(0).rotationId();
- }
-
- private static String rotationStateString(RotationState state) {
- return switch (state) {
- case in: yield "IN";
- case out: yield "OUT";
- case unknown: yield "UNKNOWN";
- };
- }
-
- private static String endpointScopeString(Endpoint.Scope scope) {
- return switch (scope) {
- case weighted: yield "weighted";
- case application: yield "application";
- case global: yield "global";
- case zone: yield "zone";
- };
- }
-
- private static String routingMethodString(RoutingMethod method) {
- return switch (method) {
- case exclusive: yield "exclusive";
- case sharedLayer4: yield "sharedLayer4";
- };
- }
-
- private static <T> T getAttribute(HttpRequest request, String attributeName, Class<T> cls) {
- return Optional.ofNullable(request.getJDiscRequest().context().get(attributeName))
- .filter(cls::isInstance)
- .map(cls::cast)
- .orElseThrow(() -> new IllegalArgumentException("Attribute '" + attributeName + "' was not set on request"));
- }
-
- /** Returns whether given request is by an operator */
- private static boolean isOperator(HttpRequest request) {
- var securityContext = getAttribute(request, SecurityContext.ATTRIBUTE_NAME, SecurityContext.class);
- return securityContext.roles().stream()
- .map(Role::definition)
- .anyMatch(definition -> definition == RoleDefinition.hostedOperator);
- }
-
- private void ensureApplicationExists(TenantAndApplicationId id, HttpRequest request) {
- if (controller.applications().getApplication(id).isEmpty()) {
- if (controller.system().isPublic() || hasOktaContext(request)) {
- log.fine("Application does not exist in public, creating: " + id);
- var credentials = accessControlRequests.credentials(id.tenant(), null /* not used on public */ , request.getJDiscRequest());
- controller.applications().createApplication(id, credentials);
- } else {
- log.fine("Application does not exist in hosted, failing: " + id);
- throw new IllegalArgumentException("Application does not exist. Create application in Console first.");
- }
- }
- }
-
- private boolean hasOktaContext(HttpRequest request) {
- try {
- OAuthCredentials.fromOktaRequestContext(request.getJDiscRequest().context());
- return true;
- } catch (IllegalArgumentException e) {
- return false;
- }
- }
-
- private List<Deployment> sortedDeployments(Collection<Deployment> deployments, DeploymentInstanceSpec spec) {
- List<ZoneId> productionZones = spec.zones().stream()
- .filter(z -> z.region().isPresent())
- .map(z -> ZoneId.from(z.environment(), z.region().get()))
- .toList();
- return deployments.stream()
- .sorted(comparingInt(deployment -> productionZones.indexOf(deployment.zone())))
- .collect(collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
- }
-
-}
-
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/HtmlResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/HtmlResponse.java
deleted file mode 100644
index 3bf2f070f97..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/HtmlResponse.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import com.yahoo.container.jdisc.HttpResponse;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-
-/**
- * @author freva
- */
-public class HtmlResponse extends HttpResponse {
-
- private final String content;
-
- public HtmlResponse(String content) {
- super(200);
- this.content = content;
- }
-
- @Override
- public void render(OutputStream stream) throws IOException {
- stream.write(content.getBytes(StandardCharsets.UTF_8));
- }
-
- @Override
- public String getContentType() { return "text/html"; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
deleted file mode 100644
index 18221d82e44..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelper.java
+++ /dev/null
@@ -1,533 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.DeploymentSpec.ChangeBlocker;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.NotExistsException;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.ConvergenceSummary;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus.DelayCause;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus.Readiness;
-import com.yahoo.vespa.hosted.controller.deployment.JobController;
-import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.Run.Reason;
-import com.yahoo.vespa.hosted.controller.deployment.RunLog;
-import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-import com.yahoo.vespa.hosted.controller.deployment.Submission;
-import com.yahoo.vespa.hosted.controller.deployment.Versions;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.URI;
-import java.time.Instant;
-import java.time.format.TextStyle;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.NavigableMap;
-import java.util.Optional;
-import java.util.stream.Stream;
-
-import static com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy.canary;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.copyVespaLogs;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installInitialReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installReal;
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.broken;
-import static com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence.normal;
-import static java.util.Comparator.reverseOrder;
-
-/**
- * Implements the REST API for the job controller delegated from the Application API.
- *
- * @see JobController
- * @see ApplicationApiHandler
- *
- * @author smorgrav
- * @author jonmv
- */
-class JobControllerApiHandlerHelper {
-
- /**
- * @return Response with all job types that have recorded runs for the application _and_ the status for the last run of that type
- */
- static HttpResponse jobTypeResponse(Controller controller, ApplicationId id, URI baseUriForJobs) {
- Slime slime = new Slime();
- Cursor responseObject = slime.setObject();
-
- Cursor jobsArray = responseObject.setArray("deployment");
- JobType.allIn(controller.zoneRegistry()).stream()
- .filter(type -> type.environment().isManuallyDeployed())
- .map(devType -> new JobId(id, devType))
- .forEach(job -> {
- Collection<Run> runs = controller.jobController().runs(job).descendingMap().values();
- if (runs.isEmpty())
- return;
-
- Cursor jobObject = jobsArray.addObject();
- jobObject.setString("jobName", job.type().jobName());
- toSlime(jobObject.setArray("runs"), runs, controller.applications().requireApplication(TenantAndApplicationId.from(id)), 10, baseUriForJobs);
- });
-
- return new SlimeJsonResponse(slime);
- }
-
- /** Returns a response with the runs for the given job type. */
- static HttpResponse runResponse(Controller controller, JobId id, Optional<String> limitStr, URI baseUriForJobType) {
- Slime slime = new Slime();
- Cursor cursor = slime.setObject();
- Application application = controller.applications().requireApplication(TenantAndApplicationId.from(id.application()));
- NavigableMap<RunId, Run> runs = controller.jobController().runs(id).descendingMap();
-
- int limit = limitStr.map(Integer::parseInt).orElse(Integer.MAX_VALUE);
- toSlime(cursor.setArray("runs"), runs.values(), application, limit, baseUriForJobType);
- Optional.ofNullable(runs.lastEntry())
- .map(entry -> new DeploymentId(id.application(), entry.getValue().id().job().type().zone())) // Urgh, must use a job with actual zone.
- .flatMap(deployment -> controller.applications().decideCloudAccountOf(deployment, application.deploymentSpec()))
- .ifPresent(cloudAccount -> cursor.setObject("enclave").setString("cloudAccount", cloudAccount.value()));
-
- return new SlimeJsonResponse(slime);
- }
-
- /**
- * @return Response with logs from a single run
- */
- static HttpResponse runDetailsResponse(JobController jobController, RunId runId, String after) {
- Slime slime = new Slime();
- Cursor detailsObject = slime.setObject();
-
- Run run = jobController.run(runId);
- detailsObject.setBool("active", ! run.hasEnded());
- detailsObject.setString("status", nameOf(run.status()));
- run.reason().reason().ifPresent(reason -> detailsObject.setString("reason", reason));
- run.reason().dependent().ifPresent(dependent -> {
- Cursor dependentObject = detailsObject.setObject("dependent");
- dependentObject.setString("instance", dependent.application().instance().value());
- dependentObject.setString("region", dependent.type().zone().region().value());
- run.reason().change().flatMap(Change::platform).ifPresent(platform -> dependentObject.setString("platform", platform.toFullString()));
- run.reason().change().flatMap(Change::revision).ifPresent(revision -> dependentObject.setLong("build", revision.number()));
- });
- try {
- jobController.updateTestLog(runId);
- jobController.updateVespaLog(runId);
- }
- catch (RuntimeException ignored) { } // Return response when this fails, which it does when, e.g., logserver is booting.
-
- RunLog runLog = (after == null ? jobController.details(runId) : jobController.details(runId, Long.parseLong(after)))
- .orElseThrow(() -> new NotExistsException(Text.format(
- "No run details exist for application: %s, job type: %s, number: %d",
- runId.application().toShortString(), runId.type().jobName(), runId.number())));
-
- Cursor logObject = detailsObject.setObject("log");
- for (Step step : Step.values()) {
- if ( ! runLog.get(step).isEmpty())
- toSlime(logObject.setArray(step.name()), runLog.get(step));
- }
- runLog.lastId().ifPresent(id -> detailsObject.setLong("lastId", id));
-
- Cursor stepsObject = detailsObject.setObject("steps");
- run.steps().forEach((step, info) -> {
- Cursor stepCursor = stepsObject.setObject(step.name());
- stepCursor.setString("status", info.status().name());
- info.startTime().ifPresent(startTime -> stepCursor.setLong("startMillis", startTime.toEpochMilli()));
- run.convergenceSummary().ifPresent(summary -> {
- // If initial installation never succeeded, but is part of the job, summary concerns it.
- // If initial succeeded, or is not part of this job, summary concerns upgrade installation.
- if ( step == installInitialReal && info.status() != succeeded
- || step == installReal && run.stepStatus(installInitialReal).map(status -> status == succeeded).orElse(true))
- toSlime(stepCursor.setObject("convergence"), summary);
- });
- });
-
- // If a test report is available, include it in the response.
- Optional<String> testReport = jobController.getTestReports(runId);
- testReport.map(SlimeUtils::jsonToSlime)
- .map(Slime::get)
- .ifPresent(reportArrayCursor -> SlimeUtils.copyArray(reportArrayCursor, detailsObject.setArray("testReports")));
-
- boolean logsStored = run.stepStatus(copyVespaLogs).map(succeeded::equals).orElse(false);
- if (run.hasStep(copyVespaLogs) && ! runId.type().isProduction() && JobController.deploymentCompletedAt(run, false).isPresent())
- detailsObject.setBool("vespaLogsActive", ! logsStored);
-
- if (runId.type().isTest() && JobController.deploymentCompletedAt(run, true).isPresent())
- detailsObject.setBool("testerLogsActive", ! logsStored);
-
- return new SlimeJsonResponse(slime);
- }
-
- /** Proxies a Vespa log request for a run to S3 once logs have been copied, or to logserver before this. */
- static HttpResponse vespaLogsResponse(JobController jobController, RunId runId, long fromMillis, boolean tester) {
- return new HttpResponse(200) {
- @Override public void render(OutputStream out) throws IOException {
- try (InputStream logs = jobController.getVespaLogs(runId, fromMillis, tester)) {
- logs.transferTo(out);
- }
- }
- };
- }
-
- private static void toSlime(Cursor summaryObject, ConvergenceSummary summary) {
- summaryObject.setLong("nodes", summary.nodes());
- summaryObject.setLong("down", summary.down());
- summaryObject.setLong("needPlatformUpgrade", summary.needPlatformUpgrade());
- summaryObject.setLong("upgrading", summary.upgradingPlatform());
- summaryObject.setLong("needReboot", summary.needReboot());
- summaryObject.setLong("rebooting", summary.rebooting());
- summaryObject.setLong("needRestart", summary.needRestart());
- summaryObject.setLong("restarting", summary.restarting());
- summaryObject.setLong("upgradingOs", summary.upgradingOs());
- summaryObject.setLong("upgradingFirmware", summary.upgradingFirmware());
- summaryObject.setLong("services", summary.services());
- summaryObject.setLong("needNewConfig", summary.needNewConfig());
- summaryObject.setLong("retiring", summary.retiring());
- }
-
- private static void toSlime(Cursor entryArray, List<LogEntry> entries) {
- entries.forEach(entry -> toSlime(entryArray.addObject(), entry));
- }
-
- private static void toSlime(Cursor entryObject, LogEntry entry) {
- entryObject.setLong("at", entry.at().toEpochMilli());
- entryObject.setString("type", entry.type().name());
- entryObject.setString("message", entry.message());
- }
-
- /**
- * Unpack payload and submit to job controller. Defaults instance to 'default' and renders the
- * application version on success.
- *
- * @return Response with the new application version
- */
- static HttpResponse submitResponse(JobController jobController, TenantAndApplicationId id, Submission submission, long projectId) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- ApplicationVersion submitted = jobController.submit(id, submission, projectId);
- String skipped = submitted.shouldSkip()
- ? "; only applying deployment spec changes, as this build is otherwise equal to the previous"
- : "";
- root.setString("message", "application " + submitted + skipped);
- root.setLong("build", submitted.buildNumber());
- return new SlimeJsonResponse(slime);
- }
-
- /** Aborts any job of the given type. */
- static HttpResponse abortJobResponse(JobController jobs, HttpRequest request, ApplicationId id, JobType type) {
- Slime slime = new Slime();
- Cursor responseObject = slime.setObject();
- Optional<Run> run = jobs.last(id, type).flatMap(last -> jobs.active(last.id()));
- if (run.isPresent()) {
- jobs.abort(run.get().id(), "aborted by " + request.getJDiscRequest().getUserPrincipal().getName(), true);
- responseObject.setString("message", "Aborting " + run.get().id());
- }
- else
- responseObject.setString("message", "Nothing to abort.");
- return new SlimeJsonResponse(slime);
- }
-
- private static String nameOf(RunStatus status) {
- return switch (status) {
- case reset, running -> "running";
- case cancelled, aborted -> "aborted";
- case error -> "error";
- case testFailure -> "testFailure";
- case noTests -> "noTests";
- case endpointCertificateTimeout -> "endpointCertificateTimeout";
- case nodeAllocationFailure -> "nodeAllocationFailure";
- case installationFailed -> "installationFailed";
- case invalidApplication, deploymentFailed -> "deploymentFailed";
- case success -> "success";
- case quotaExceeded -> "quotaExceeded";
- };
- }
-
- /**
- * Returns response with all job types that have recorded runs for the application
- * _and_ the status for the last run of that type
- */
- static HttpResponse overviewResponse(Controller controller, TenantAndApplicationId id, URI baseUriForDeployments) {
- Application application = controller.applications().requireApplication(id);
- DeploymentStatus status = controller.jobController().deploymentStatus(application);
-
- Slime slime = new Slime();
- Cursor responseObject = slime.setObject();
- responseObject.setString("tenant", id.tenant().value());
- responseObject.setString("application", id.application().value());
- application.projectId().ifPresent(projectId -> responseObject.setLong("projectId", projectId));
-
- Map<JobId, List<DeploymentStatus.Job>> jobsToRun = status.jobsToRun();
- Cursor stepsArray = responseObject.setArray("steps");
- VersionStatus versionStatus = controller.readVersionStatus();
- for (DeploymentStatus.StepStatus stepStatus : status.allSteps()) {
- Change change = status.application().require(stepStatus.instance()).change();
- Cursor stepObject = stepsArray.addObject();
- stepObject.setString("type", stepStatus.type().name());
- stepStatus.dependencies().stream()
- .map(status.allSteps()::indexOf)
- .forEach(stepObject.setArray("dependencies")::addLong);
- stepObject.setBool("declared", stepStatus.isDeclared());
- stepObject.setString("instance", stepStatus.instance().value());
-
- // TODO: recursively search dependents for what is the relevant partial change when this is a delay step ...
- Instant now = controller.clock().instant();
- Readiness readiness = stepStatus.pausedUntil().okAt(now)
- ? stepStatus.job().map(jobsToRun::get).map(job -> job.get(0).readiness())
- .orElse(stepStatus.readiness(change))
- : stepStatus.pausedUntil();
- if (readiness.ok()) {
- // TODO jonmv: remove after UI changes.
- stepObject.setLong("readyAt", readiness.at().toEpochMilli());
-
- if ( ! readiness.okAt(now)) stepObject.setLong("delayedUntil", readiness.at().toEpochMilli());
- }
-
- // TODO jonmv: remove after UI changes.
- if (readiness.cause() == DelayCause.coolingDown) stepObject.setLong("coolingDownUntil", readiness.at().toEpochMilli());
- if (readiness.cause() == DelayCause.paused) stepObject.setLong("pausedUntil", readiness.at().toEpochMilli());
-
- Readiness platformReadiness = stepStatus.blockedUntil(Change.of(controller.systemVersion(versionStatus))); // Dummy version — just anything with a platform.
- if ( ! platformReadiness.okAt(now))
- stepObject.setLong("platformBlockedUntil", platformReadiness.at().toEpochMilli());
- Readiness applicationReadiness = stepStatus.blockedUntil(Change.of(RevisionId.forProduction(1))); // Dummy version — just anything with an application.
- if ( ! applicationReadiness.okAt(now))
- stepObject.setLong("applicationBlockedUntil", applicationReadiness.at().toEpochMilli());
-
- if (stepStatus.type() == DeploymentStatus.StepType.delay)
- stepStatus.completedAt(change).ifPresent(completed -> stepObject.setLong("completedAt", completed.toEpochMilli()));
-
- if (stepStatus.type() == DeploymentStatus.StepType.instance) {
- Cursor deployingObject = stepObject.setObject("deploying");
- if ( ! change.isEmpty()) {
- change.platform().ifPresent(version -> deployingObject.setString("platform", version.toFullString()));
- change.revision().ifPresent(revision -> toSlime(deployingObject.setObject("application"), application.revisions().get(revision)));
- if (change.isPlatformPinned()) deployingObject.setBool("pinned", true);
- if (change.isPlatformPinned()) deployingObject.setBool("platformPinned", true);
- if (change.isRevisionPinned()) deployingObject.setBool("revisionPinned", true);
- }
-
- Cursor latestVersionsObject = stepObject.setObject("latestVersions");
- List<ChangeBlocker> blockers = application.deploymentSpec().requireInstance(stepStatus.instance()).changeBlocker();
- var deployments = application.require(stepStatus.instance()).productionDeployments().values();
- List<VespaVersion> availablePlatforms = availablePlatforms(versionStatus.versions(),
- application.deploymentSpec().requireInstance(stepStatus.instance()).upgradePolicy());
- if ( ! availablePlatforms.isEmpty()) {
- Cursor latestPlatformObject = latestVersionsObject.setObject("platform");
- VespaVersion latestPlatform = availablePlatforms.get(0);
- latestPlatformObject.setString("platform", latestPlatform.versionNumber().toFullString());
- latestPlatformObject.setLong("at", latestPlatform.committedAt().toEpochMilli());
- latestPlatformObject.setBool("upgrade", change.platform().map(latestPlatform.versionNumber()::isAfter).orElse(true) && deployments.isEmpty()
- || deployments.stream().anyMatch(deployment -> deployment.version().isBefore(latestPlatform.versionNumber())));
-
- Cursor availableArray = latestPlatformObject.setArray("available");
- boolean isUpgrade = true;
- for (VespaVersion available : availablePlatforms) {
- if ( deployments.stream().anyMatch(deployment -> deployment.version().isAfter(available.versionNumber()))
- || deployments.stream().noneMatch(deployment -> deployment.version().isBefore(available.versionNumber())) && ! deployments.isEmpty()
- || status.hasCompleted(stepStatus.instance(), Change.of(available.versionNumber()))
- || change.platform().map(available.versionNumber()::compareTo).orElse(1) < 0)
- isUpgrade = false;
-
- Cursor platformObject = availableArray.addObject();
- platformObject.setString("platform", available.versionNumber().toFullString());
- platformObject.setBool("upgrade", isUpgrade || change.platform().map(available.versionNumber()::equals).orElse(false));
- }
- toSlime(latestPlatformObject.setArray("blockers"), blockers.stream().filter(ChangeBlocker::blocksVersions));
- }
- List<ApplicationVersion> availableApplications = new ArrayList<>(application.revisions().deployable(false));
- if ( ! availableApplications.isEmpty()) {
- var latestApplication = availableApplications.get(0);
- Cursor latestApplicationObject = latestVersionsObject.setObject("application");
- toSlime(latestApplicationObject.setObject("application"), latestApplication);
- latestApplicationObject.setLong("at", latestApplication.buildTime().orElse(Instant.EPOCH).toEpochMilli());
- latestApplicationObject.setBool("upgrade", change.revision().map(latestApplication.id()::compareTo).orElse(1) > 0 && deployments.isEmpty()
- || deployments.stream().anyMatch(deployment -> deployment.revision().compareTo(latestApplication.id()) < 0));
-
- Cursor availableArray = latestApplicationObject.setArray("available");
- for (ApplicationVersion available : availableApplications)
- toSlime(availableArray.addObject().setObject("application"), available);
-
- toSlime(latestApplicationObject.setArray("blockers"), blockers.stream().filter(ChangeBlocker::blocksRevisions));
- }
- }
-
- boolean showDelayCause = true;
- if (stepStatus.job().isPresent()) {
- JobId job = stepStatus.job().get();
- stepObject.setString("jobName", job.type().jobName());
- URI baseUriForJob = baseUriForDeployments.resolve(baseUriForDeployments.getPath() +
- "/../instance/" + job.application().instance().value() +
- "/job/" + job.type().jobName()).normalize();
- stepObject.setString("url", baseUriForJob.toString());
- stepObject.setString("environment", job.type().environment().value());
- if ( ! job.type().environment().isTest()) {
- stepObject.setString("region", job.type().zone().value());
- }
-
- if (job.type().isProduction() && job.type().isDeployment()) {
- status.deploymentFor(job).ifPresent(deployment -> {
- stepObject.setString("currentPlatform", deployment.version().toFullString());
- toSlime(stepObject.setObject("currentApplication"), application.revisions().get(deployment.revision()));
- });
- }
-
- JobStatus jobStatus = status.jobs().get(job).get();
- Cursor toRunArray = stepObject.setArray("toRun");
- showDelayCause = readiness.cause() == DelayCause.paused;
- for (DeploymentStatus.Job jobToRun : jobsToRun.getOrDefault(job, List.of())) {
- boolean running = jobStatus.lastTriggered()
- .map(run -> jobStatus.isRunning()
- && jobToRun.versions().targetsMatch(run.versions())
- && (job.type().isProduction() || jobToRun.versions().sourcesMatchIfPresent(run.versions())))
- .orElse(false);
- if (running)
- continue; // Run will be contained in the "runs" array.
-
- showDelayCause = true;
- Cursor runObject = toRunArray.addObject();
- toSlime(runObject, jobToRun.versions(), jobToRun.reason(), application);
- }
-
- if ( ! jobStatus.runs().isEmpty())
- controller.applications().decideCloudAccountOf(new DeploymentId(job.application(),
- jobStatus.runs().lastEntry().getValue().id().job().type().zone()), // Urgh, must use a job with actual zone.
- status.application().deploymentSpec())
- .ifPresent(cloudAccount -> stepObject.setObject("enclave").setString("cloudAccount", cloudAccount.value()));
-
-
- toSlime(stepObject.setArray("runs"), jobStatus.runs().descendingMap().values(), application, 10, baseUriForJob);
- }
- stepObject.setString("delayCause",
- ! showDelayCause
- ? (String) null
- : switch (readiness.cause()) {
- case none -> null;
- case invalidPackage -> "invalidPackage";
- case paused -> "paused";
- case coolingDown -> "coolingDown";
- case changeBlocked -> "changeBlocked";
- case blocked -> "blocked";
- case running -> "running";
- case notReady -> "notReady";
- case unverified -> "unverified";
- });
- }
-
- Cursor buildsArray = responseObject.setArray("builds");
- application.revisions().withPackage().stream().sorted(reverseOrder()).forEach(version -> toRichSlime(buildsArray.addObject(), version));
-
- return new SlimeJsonResponse(slime);
- }
-
- static void toRichSlime(Cursor versionObject, ApplicationVersion version) {
- toSlime(versionObject, version);
- version.description().ifPresent(description -> versionObject.setString("description", description));
- if (version.risk() != 0) versionObject.setLong("risk", version.risk());
- versionObject.setBool("deployable", version.isDeployable());
- version.submittedAt().ifPresent(submittedAt -> versionObject.setLong("submittedAt", submittedAt.toEpochMilli()));
- }
-
- static void toSlime(Cursor versionObject, ApplicationVersion version) {
- versionObject.setLong("build", version.buildNumber());
- version.compileVersion().ifPresent(platform -> versionObject.setString("compileVersion", platform.toFullString()));
- version.sourceUrl().ifPresent(url -> versionObject.setString("sourceUrl", url));
- version.commit().ifPresent(commit -> versionObject.setString("commit", commit));
- }
-
- private static void toSlime(Cursor versionsObject, Versions versions, Application application) {
- versionsObject.setString("targetPlatform", versions.targetPlatform().toFullString());
- toSlime(versionsObject.setObject("targetApplication"), application.revisions().get(versions.targetRevision()));
- versions.sourcePlatform().ifPresent(platform -> versionsObject.setString("sourcePlatform", platform.toFullString()));
- versions.sourceRevision().ifPresent(revision -> toSlime(versionsObject.setObject("sourceApplication"), application.revisions().get(revision)));
- }
-
- private static void toSlime(Cursor blockersArray, Stream<ChangeBlocker> blockers) {
- blockers.forEach(blocker -> {
- Cursor blockerObject = blockersArray.addObject();
- blocker.window().days().stream()
- .map(day -> day.getDisplayName(TextStyle.SHORT, Locale.ENGLISH))
- .forEach(blockerObject.setArray("days")::addString);
- blocker.window().hours()
- .forEach(blockerObject.setArray("hours")::addLong);
- blockerObject.setString("zone", blocker.window().zone().toString());
- });
- }
-
- private static List<VespaVersion> availablePlatforms(List<VespaVersion> versions, DeploymentSpec.UpgradePolicy policy) {
- int i;
- for (i = versions.size(); i-- > 0; )
- if (versions.get(i).isSystemVersion())
- break;
-
- if (i < 0)
- return List.of();
-
- List<VespaVersion> candidates = new ArrayList<>();
- VespaVersion.Confidence required = policy == canary ? broken : normal;
- for (int j = i; j >= 0; j--)
- if (versions.get(j).confidence().equalOrHigherThan(required))
- candidates.add(versions.get(j));
-
- if (candidates.isEmpty())
- candidates.add(versions.get(i));
-
- return candidates;
- }
-
- private static void toSlime(Cursor runObject, Versions versions, Reason reason, Application application) {
- reason.reason().ifPresent(because -> runObject.setString("reason", because));
- reason.dependent().ifPresent(dependent -> {
- Cursor dependentObject = runObject.setObject("dependent");
- dependentObject.setString("instance", dependent.application().instance().value());
- dependentObject.setString("region", dependent.type().zone().region().value());
- reason.change().flatMap(Change::platform).ifPresent(platform -> dependentObject.setString("platform", platform.toFullString()));
- reason.change().flatMap(Change::revision).ifPresent(revision -> dependentObject.setLong("build", revision.number()));
- });
- toSlime(runObject.setObject("versions"), versions, application);
- }
-
- private static void toSlime(Cursor runsArray, Collection<Run> runs, Application application, int limit, URI baseUriForJob) {
- runs.stream().limit(limit).forEach(run -> {
- Cursor runObject = runsArray.addObject();
- runObject.setLong("id", run.id().number());
- runObject.setString("url", baseUriForJob.resolve(baseUriForJob.getPath() + "/run/" + run.id().number()).toString());
- runObject.setLong("start", run.start().toEpochMilli());
- run.end().ifPresent(end -> runObject.setLong("end", end.toEpochMilli()));
- runObject.setString("status", nameOf(run.status()));
- toSlime(runObject, run.versions(), run.reason(), application);
- run.cloudAccount().filter(account -> ! account.isUnspecified())
- .ifPresent(cloudAccount -> runObject.setObject("enclave").setString("cloudAccount", cloudAccount.value()));
- Cursor runStepsArray = runObject.setArray("steps");
- run.steps().forEach((step, info) -> {
- Cursor runStepObject = runStepsArray.addObject();
- runStepObject.setString("name", step.name());
- runStepObject.setString("status", info.status().name());
- });
- });
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java
deleted file mode 100644
index 35eb495a564..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParser.java
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource;
-import org.apache.commons.fileupload.MultipartStream;
-import org.apache.commons.fileupload.ParameterParser;
-import org.apache.commons.fileupload.util.Streams;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * Provides reading a multipart/form-data request type into a map of bytes for each part,
- * indexed by the parts (form field) name.
- *
- * @author bratseth
- */
-public class MultipartParser {
-
- private final long maxDataLength;
-
- public MultipartParser() {
- this(2 * (long) Math.pow(1024, 3)); // 2 GB
- }
-
- MultipartParser(long maxDataLength) {
- this.maxDataLength = maxDataLength;
- }
-
- /**
- * Parses the given multipart request and returns all the parts indexed by their name.
- *
- * @throws IllegalArgumentException if this request is not a well-formed request with Content-Type multipart/form-data
- */
- public Map<String, byte[]> parse(HttpRequest request) {
- return parse(request.getHeader("Content-Type"), request.getData(), request.getUri());
- }
-
- /**
- * Parses the given data stream for the given uri using the provided content-type header to determine boundaries.
- *
- * @throws IllegalArgumentException if this is not a well-formed request with Content-Type multipart/form-data
- */
- public Map<String, byte[]> parse(String contentTypeHeader, InputStream data, URI uri) {
- try {
- LimitedOutputStream output = new LimitedOutputStream(maxDataLength);
- ParameterParser parameterParser = new ParameterParser();
- Map<String, String> contentType = parameterParser.parse(contentTypeHeader, ';');
- if (contentType.containsKey("application/zip")) {
- Streams.copy(data, output, false);
- return Map.of(EnvironmentResource.APPLICATION_ZIP, output.toByteArray());
- }
- if ( ! contentType.containsKey("multipart/form-data"))
- throw new IllegalArgumentException("Expected a multipart or application/zip message, but got Content-Type: " + contentTypeHeader);
- String boundary = contentType.get("boundary");
- if (boundary == null)
- throw new IllegalArgumentException("Missing boundary property in Content-Type header");
- MultipartStream multipartStream = new MultipartStream(data, boundary.getBytes(), 1 << 20, null);
- boolean nextPart = multipartStream.skipPreamble();
- Map<String, byte[]> parts = new HashMap<>();
- while (nextPart) {
- String[] headers = multipartStream.readHeaders().split("\r\n");
- String contentDispositionContent = findContentDispositionHeader(headers);
- if (contentDispositionContent == null)
- throw new IllegalArgumentException("Missing Content-Disposition header in a multipart body part");
- Map<String, String> contentDisposition = parameterParser.parse(contentDispositionContent, ';');
- multipartStream.readBodyData(output);
- parts.put(contentDisposition.get("name"), output.toByteArray());
- output.reset();
- nextPart = multipartStream.readBoundary();
- }
- return parts;
- }
- catch (MultipartStream.MalformedStreamException e) {
- throw new IllegalArgumentException("Malformed multipart/form-data request", e);
- }
- catch (IOException e) {
- throw new IllegalArgumentException("IO error reading multipart request " + uri, e);
- }
- }
-
- private String findContentDispositionHeader(String[] headers) {
- String contentDisposition = "Content-Disposition:";
- for (String header : headers) {
- if (header.length() < contentDisposition.length()) continue;
- if ( ! header.substring(0, contentDisposition.length()).equalsIgnoreCase(contentDisposition)) continue;
- return header.substring(contentDisposition.length() + 1);
- }
- return null;
- }
-
- /** A {@link java.io.ByteArrayOutputStream} that limits the number of bytes written to it */
- private static class LimitedOutputStream extends ByteArrayOutputStream {
-
- private long remaining;
-
- /** Create a new OutputStream that can fit up to len bytes */
- private LimitedOutputStream(long len) {
- this.remaining = len;
- }
-
- @Override
- public synchronized void write(int b) {
- requireCapacity(1);
- super.write(b);
- remaining--;
- }
-
- @Override
- public synchronized void write(byte[] b, int off, int len) {
- requireCapacity(len);
- super.write(b, off, len);
- remaining -= len;
- }
-
- private void requireCapacity(int len) {
- if (len > remaining) throw new IllegalArgumentException("Too many bytes to write");
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ZipResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ZipResponse.java
deleted file mode 100644
index 73f9db7165c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/application/ZipResponse.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import com.yahoo.container.jdisc.HttpResponse;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-/**
- * A HTTP response containing a named ZIP file.
- *
- * @author mpolden
- */
-public class ZipResponse extends HttpResponse {
-
- private final InputStream zipContent;
-
- public ZipResponse(String filename, InputStream zipContent) {
- super(200);
- this.zipContent = zipContent;
- this.headers().add("Content-Disposition", "attachment; filename=\"" + filename + "\"");
- }
-
- @Override
- public String getContentType() {
- return "application/zip";
- }
-
- @Override
- public void render(OutputStream outputStream) throws IOException {
- try (zipContent) {
- zipContent.transferTo(outputStream);
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java
deleted file mode 100644
index 2ff0c1ab05c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiHandler.java
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.athenz;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.restapi.ResourceResponse;
-import com.yahoo.restapi.RestApi;
-import com.yahoo.restapi.RestApiRequestHandler;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.integration.entity.EntityService;
-import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
-
-import java.util.Map;
-import java.util.logging.Logger;
-
-import static com.yahoo.restapi.RestApi.route;
-
-/**
- * This API proxies requests to an Athenz server.
- *
- * @author jonmv
- */
-@SuppressWarnings("unused") // Handler
-public class AthenzApiHandler extends RestApiRequestHandler<AthenzApiHandler> {
-
- private final static Logger log = Logger.getLogger(AthenzApiHandler.class.getName());
-
- private final AthenzFacade athenz;
- private final AthenzDomain sandboxDomain;
- private final EntityService properties;
-
- @Inject
- public AthenzApiHandler(Context parentCtx, AthenzFacade athenz, Controller controller) {
- super(parentCtx, AthenzApiHandler::createRestApi);
- this.athenz = athenz;
- this.sandboxDomain = new AthenzDomain(sandboxDomainIn(controller.system()));
- this.properties = controller.serviceRegistry().entityService();
- }
-
- private static RestApi createRestApi(AthenzApiHandler self) {
- return RestApi.builder()
- .addRoute(route("/athenz/v1")
- .get(self::root))
- .addRoute(route("/athenz/v1/domains")
- .get(self::domainList))
- .addRoute(route("/athenz/v1/properties")
- .get(self::properties))
- .addRoute(route("/athenz/v1/user")
- .post(self::signup))
- .build();
- }
-
- private HttpResponse root(RestApi.RequestContext ctx) {
- return new ResourceResponse(ctx.request(), "domains", "properties");
- }
-
- private Slime properties(RestApi.RequestContext ctx) {
- Slime slime = new Slime();
- Cursor response = slime.setObject();
- Cursor array = response.setArray("properties");
- for (Map.Entry<PropertyId, Property> entry : properties.listProperties().entrySet()) {
- Cursor propertyObject = array.addObject();
- propertyObject.setString("propertyid", entry.getKey().id());
- propertyObject.setString("property", entry.getValue().id());
- }
- return slime;
- }
-
- private Slime domainList(RestApi.RequestContext ctx) {
- Slime slime = new Slime();
- Cursor array = slime.setObject().setArray("data");
- for (AthenzDomain athenzDomain : athenz.getDomainList(ctx.queryParameters().getString("prefix").orElse(null)))
- array.addString(athenzDomain.getName());
-
- return slime;
- }
-
- private String signup(RestApi.RequestContext ctx) {
- AthenzUser user = athenzUser(ctx);
- athenz.addTenantAdmin(sandboxDomain, user);
- return "User '" + user.getName() + "' added to admin role of '" + sandboxDomain.getName() + "'";
- }
-
- private static AthenzUser athenzUser(RestApi.RequestContext ctx) {
- return ctx.userPrincipal()
- .filter(AthenzPrincipal.class::isInstance)
- .map(AthenzPrincipal.class::cast)
- .map(AthenzPrincipal::getIdentity)
- .filter(AthenzUser.class::isInstance)
- .map(AthenzUser.class::cast)
- .orElseThrow(() -> new IllegalArgumentException("No Athenz user principal on request"));
- }
-
- static String sandboxDomainIn(SystemName system) {
- switch (system) {
- case main: return "vespa.vespa.tenants.sandbox";
- case cd: return "vespa.vespa.cd.tenants.sandbox";
- default: throw new IllegalArgumentException("No sandbox domain in system '" + system + "'");
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
deleted file mode 100644
index bdd89abfa4c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2.java
+++ /dev/null
@@ -1,683 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.billing;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.MessageResponse;
-import com.yahoo.restapi.RestApi;
-import com.yahoo.restapi.RestApiException;
-import com.yahoo.restapi.RestApiRequestHandler;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.slime.Type;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.TenantController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingReporter;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.CollectionMethod;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.StatusHistory;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.vespa.hosted.controller.tenant.BillingReference;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.math.BigDecimal;
-import java.time.Clock;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.logging.Logger;
-
-/**
- * @author ogronnesby
- */
-public class BillingApiHandlerV2 extends RestApiRequestHandler<BillingApiHandlerV2> {
-
- private static final Logger log = Logger.getLogger(BillingApiHandlerV2.class.getName());
-
- private static final String[] CSV_INVOICE_HEADER = new String[]{ "ID", "Tenant", "From", "To", "CpuHours", "MemoryHours", "DiskHours", "Cpu", "Memory", "Disk", "Additional" };
-
- private final ApplicationController applications;
- private final TenantController tenants;
- private final BillingController billing;
- private final BillingReporter billingReporter;
- private final PlanRegistry planRegistry;
- private final Clock clock;
-
- public BillingApiHandlerV2(ThreadedHttpRequestHandler.Context context, Controller controller) {
- super(context, BillingApiHandlerV2::createRestApi);
- this.applications = controller.applications();
- this.tenants = controller.tenants();
- this.billing = controller.serviceRegistry().billingController();
- this.planRegistry = controller.serviceRegistry().planRegistry();
- this.clock = controller.serviceRegistry().clock();
- this.billingReporter = controller.serviceRegistry().billingReporter();
- }
-
- private static RestApi createRestApi(BillingApiHandlerV2 self) {
- return RestApi.builder()
- /*
- * This is the API that is tenant agnostic
- */
- .addRoute(RestApi.route("/billing/v2/countries")
- .get(self::acceptedCountries))
-
- /*
- * This is the API that is available to tenants to view their status
- */
- .addRoute(RestApi.route("/billing/v2/tenant/{tenant}")
- .get(self::tenant)
- .patch(Slime.class, self::patchTenant))
- .addRoute(RestApi.route("/billing/v2/tenant/{tenant}/usage")
- .get(self::tenantUsage))
- .addRoute(RestApi.route("/billing/v2/tenant/{tenant}/bill")
- .get(self::tenantInvoiceList))
- .addRoute(RestApi.route("/billing/v2/tenant/{tenant}/bill/{invoice}")
- .get(self::tenantInvoice))
- /*
- * This is the API that is created for accountant role in Vespa Cloud
- */
- .addRoute(RestApi.route("/billing/v2/accountant")
- .get(self::accountant))
- .addRoute(RestApi.route("/billing/v2/accountant/preview")
- .get(self::accountantPreview))
- .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}")
- .get(self::accountantTenant))
- .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/preview")
- .get(self::previewBill)
- .post(Slime.class, self::createBill))
- .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/items")
- .get(self::additionalItems)
- .post(Slime.class, self::newAdditionalItem))
- .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/item/{item}")
- .delete(self::deleteAdditionalItem))
- .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/plan")
- .get(self::accountantTenantPlan)
- .post(Slime.class, self::setAccountantTenantPlan))
- .addRoute(RestApi.route("/billing/v2/accountant/tenant/{tenant}/collection")
- .get(self::accountantTenantCollection)
- .post(Slime.class, self::setAccountantTenantCollection))
- .addRoute(RestApi.route("/billing/v2/accountant/bill/{invoice}/summary")
- .get(self::accountantInvoiceSummary))
- .addRoute(RestApi.route("/billing/v2/accountant/bill/{invoice}/export")
- .put(Slime.class, self::putAccountantInvoiceExport))
- .addRoute(RestApi.route("/billing/v2/accountant/plans")
- .get(self::plans))
- .addExceptionMapper(RuntimeException.class, (c, e) -> ErrorResponses.logThrowing(c.request(), log, e))
- .build();
- }
-
- // ---------- AUX API -------------
-
- private SlimeJsonResponse acceptedCountries(RestApi.RequestContext ctx) {
- var response = new Slime();
- var countries = response.setObject().setArray("countries");
- billing.getAcceptedCountries().countries().forEach(country -> {
- var countryCursor = countries.addObject();
- countryCursor.setString("code", country.code());
- countryCursor.setString("name", country.displayName());
- countryCursor.setBool("taxIdMandatory", country.taxIdMandatory());
- var taxTypesCursors = countryCursor.setArray("taxTypes");
- country.taxTypes().forEach(taxType -> {
- var taxTypeCursor = taxTypesCursors.addObject();
- taxTypeCursor.setString("id", taxType.id());
- taxTypeCursor.setString("description", taxType.description());
- taxTypeCursor.setString("pattern", taxType.pattern());
- taxTypeCursor.setString("example", taxType.example());
- });
- });
- return new SlimeJsonResponse(response, /*compact*/false);
- }
-
- // ---------- TENANT API ----------
-
- private Slime tenant(RestApi.RequestContext requestContext) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
-
- var plan = planFor(tenant.name());
- var collectionMethod = billing.getCollectionMethod(tenant.name());
-
- var response = new Slime();
- var cursor = response.setObject();
- cursor.setString("tenant", tenant.name().value());
-
- toSlime(cursor.setObject("plan"), plan);
- cursor.setString("collection", collectionMethod.name());
- return response;
- }
-
- private Slime patchTenant(RestApi.RequestContext requestContext, Slime body) {
- var security = requestContext.attributes().get(SecurityContext.ATTRIBUTE_NAME)
- .map(SecurityContext.class::cast)
- .orElseThrow(() -> new RestApiException.Forbidden("Must be logged in"));
-
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
-
- var newPlan = body.get().field("plan");
- var newCollection = body.get().field("collection");
-
- if (newPlan.valid() && newPlan.type() == Type.STRING) {
- var planId = PlanId.from(newPlan.asString());
- var hasDeployments = tenantHasDeployments(tenant.name());
- var result = billing.setPlan(tenant.name(), planId, hasDeployments, false);
- if (! result.isSuccess()) {
- throw new RestApiException.Forbidden(result.getErrorMessage().get());
- }
- }
-
- if (newCollection.valid() && newCollection.type() == Type.STRING) {
- if (security.roles().contains(Role.hostedAccountant())) {
- var collection = CollectionMethod.valueOf(newCollection.asString());
- billing.setCollectionMethod(tenant.name(), collection);
- } else {
- throw new RestApiException.Forbidden("Only accountant can change billing method");
- }
- }
-
- var response = new Slime();
- var cursor = response.setObject();
- cursor.setString("tenant", tenant.name().value());
- toSlime(cursor.setObject("plan"), planFor(tenant.name()));
- cursor.setString("collection", billing.getCollectionMethod(tenant.name()).name());
- return response;
- }
-
- private Slime tenantInvoiceList(RestApi.RequestContext requestContext) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
-
- var slime = new Slime();
- invoicesSummaryToSlime(slime.setObject().setArray("invoices"), billing.getBillsForTenant(tenant.name()));
- return slime;
- }
-
- private HttpResponse tenantInvoice(RestApi.RequestContext requestContext) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
- var invoiceId = requestContext.pathParameters().getStringOrThrow("invoice");
- var format = requestContext.queryParameters().getString("format").orElse("json");
-
- var invoice = billing.getBillsForTenant(tenant.name()).stream()
- .filter(inv -> inv.id().value().equals(invoiceId))
- .findAny()
- .orElseThrow(RestApiException.NotFound::new);
-
- if (format.equals("json")) {
- var slime = new Slime();
- toSlime(slime.setObject(), invoice);
- return new SlimeJsonResponse(slime);
- }
-
- if (format.equals("csv")) {
- var csv = toCsv(invoice);
- return new CsvResponse(CSV_INVOICE_HEADER, csv);
- }
-
- throw new RestApiException.BadRequest("Unknown format: " + format);
- }
-
- private boolean tenantHasDeployments(TenantName tenant) {
- return applications.asList(tenant).stream()
- .flatMap(app -> app.instances().values().stream())
- .mapToLong(instance -> instance.deployments().size())
- .sum() > 0;
- }
-
- private Slime tenantUsage(RestApi.RequestContext requestContext) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
- var untilAt = untilParameter(requestContext);
- var usage = billing.createUncommittedBill(tenant.name(), untilAt);
- var slime = new Slime();
- usageToSlime(slime.setObject(), usage);
- return slime;
- }
-
- // --------- ACCOUNTANT API ----------
-
- private Slime accountant(RestApi.RequestContext requestContext) {
- var response = new Slime();
- var tenantsResponse = response.setObject().setArray("tenants");
-
- tenants.asList().stream().sorted(Comparator.comparing(Tenant::name)).forEach(tenant -> {
- var tenantResponse = tenantsResponse.addObject();
- tenantResponse.setString("tenant", tenant.name().value());
- toSlime(tenantResponse.setObject("plan"), planFor(tenant.name()));
- toSlime(tenantResponse.setObject("quota"), billing.getQuota(tenant.name()));
- tenantResponse.setString("collection", billing.getCollectionMethod(tenant.name()).name());
- tenantResponse.setString("lastBill", LocalDateTime.ofInstant(Instant.EPOCH, ZoneOffset.UTC).format(DateTimeFormatter.ISO_DATE));
- tenantResponse.setString("unbilled", "0.00");
- });
-
- return response;
- }
-
- private Slime accountantPreview(RestApi.RequestContext requestContext) {
- var untilAt = untilParameter(requestContext);
- var usagePerTenant = billing.createUncommittedBills(untilAt);
-
- var response = new Slime();
- var tenantsResponse = response.setObject().setArray("tenants");
-
- usagePerTenant.entrySet().stream().sorted(Comparator.comparing(x -> x.getValue().sum())).forEachOrdered(x -> {
- var tenant = x.getKey();
- var usage = x.getValue();
- var tenantResponse = tenantsResponse.addObject();
- tenantResponse.setString("tenant", tenant.value());
- toSlime(tenantResponse.setObject("plan"), planFor(tenant));
- toSlime(tenantResponse.setObject("quota"), billing.getQuota(tenant));
- tenantResponse.setString("collection", billing.getCollectionMethod(tenant).name());
- tenantResponse.setString("lastBill", usage.getStartDate().format(DateTimeFormatter.ISO_DATE));
- tenantResponse.setString("unbilled", usage.sum().toPlainString());
- });
-
- return response;
- }
-
- private Slime previewBill(RestApi.RequestContext requestContext) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
- var untilAt = untilParameter(requestContext);
-
- var usage = billing.createUncommittedBill(tenant.name(), untilAt);
-
- var slime = new Slime();
- toSlime(slime.setObject(), usage);
- return slime;
- }
-
- private HttpResponse createBill(RestApi.RequestContext requestContext, Slime slime) {
- var body = slime.get();
- var security = requestContext.attributes().get(SecurityContext.ATTRIBUTE_NAME)
- .map(SecurityContext.class::cast)
- .orElseThrow(() -> new RestApiException.Forbidden("Must be logged in"));
-
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
-
- var startAt = LocalDate.parse(getInspectorFieldOrThrow(body, "from")).atStartOfDay(ZoneOffset.UTC);
- var endAt = LocalDate.parse(getInspectorFieldOrThrow(body, "to")).plusDays(1).atStartOfDay(ZoneOffset.UTC);
-
- var invoiceId = billing.createBillForPeriod(tenant.name(), startAt, endAt, security.principal().getName());
-
- // TODO: Make a redirect to the bill itself
- return new MessageResponse("Created bill " + invoiceId.value());
- }
-
- private HttpResponse plans(RestApi.RequestContext ctx) {
- var slime = new Slime();
- var root = slime.setObject();
- var plans = root.setArray("plans");
- for (var plan : planRegistry.all()) {
- var p = plans.addObject();
- p.setString("id", plan.id().value());
- p.setString("name", plan.displayName());
- }
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse putAccountantInvoiceExport(RestApi.RequestContext ctx, Slime slime) {
- var billId = Bill.Id.of(ctx.pathParameters().getStringOrThrow("invoice"));
-
- // TODO: try to find a way to retrieve the cloud tenant from BillingControllerImpl
- var bill = billing.getBill(billId);
- var cloudTenant = tenants.require(bill.tenant(), CloudTenant.class);
-
- var exportMethod = slime.get().field("method").asString();
- var result = billingReporter.exportBill(bill, exportMethod, cloudTenant);
-
- var responseSlime = new Slime();
- responseSlime.setObject().setString("invoiceId", result);
- return new SlimeJsonResponse(responseSlime);
- }
-
- private MessageResponse deleteAdditionalItem(RestApi.RequestContext requestContext) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.get(tenantName).orElseThrow(() -> new RestApiException.NotFound("No such tenant: " + tenantName));
-
- var itemId = requestContext.pathParameters().getStringOrThrow("item");
-
- var items = billing.getUnusedLineItems(tenant.name());
- var candidate = items.stream().filter(item -> item.id().equals(itemId)).findAny();
-
- if (candidate.isEmpty()) {
- throw new RestApiException.NotFound("Could not find item with ID " + itemId);
- }
-
- billing.deleteLineItem(itemId);;
-
- return new MessageResponse("Successfully deleted line item " + itemId);
- }
-
- private MessageResponse newAdditionalItem(RestApi.RequestContext requestContext, Slime body) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.get(tenantName).orElseThrow(() -> new RestApiException.NotFound("No such tenant: " + tenantName));
-
- var inspector = body.get();
-
- var billId = SlimeUtils.optionalString(inspector.field("billId")).map(Bill.Id::of);
-
- billing.addLineItem(
- tenant.name(),
- getInspectorFieldOrThrow(inspector, "description"),
- new BigDecimal(getInspectorFieldOrThrow(inspector, "amount")),
- billId,
- requestContext.userPrincipalOrThrow().getName());
-
- return new MessageResponse("Added line item for tenant " + tenantName);
- }
-
- private Slime additionalItems(RestApi.RequestContext requestContext) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.get(tenantName).orElseThrow(() -> new RestApiException.NotFound("No such tenant: " + tenantName));
-
- var slime = new Slime();
- var items = slime.setObject().setArray("items");
-
- billing.getUnusedLineItems(tenant.name()).forEach(item -> {
- var itemCursor = items.addObject();
- toSlime(itemCursor, item);
- });
-
- return slime;
- }
-
- private MessageResponse setAccountantTenantPlan(RestApi.RequestContext requestContext, Slime body) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
-
- var planId = PlanId.from(getInspectorFieldOrThrow(body.get(), "id"));
- var response = billing.setPlan(tenant.name(), planId, false, true);
-
- if (response.isSuccess()) {
- return new MessageResponse("Plan: " + planId.value());
- } else {
- throw new RestApiException.BadRequest("Could not change plan: " + response.getErrorMessage().get());
- }
- }
-
- private Slime accountantTenantPlan(RestApi.RequestContext requestContext) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
-
- var planId = billing.getPlan(tenant.name());
- var plan = planRegistry.plan(planId);
-
- if (plan.isEmpty()) {
- throw new RestApiException.BadRequest("Plan with ID '" + planId.value() + "' does not exist");
- }
-
- var slime = new Slime();
- var root = slime.setObject();
- root.setString("id", plan.get().id().value());
- root.setString("name", plan.get().displayName());
-
- return slime;
- }
-
- private Slime accountantInvoiceSummary(RestApi.RequestContext requestContext) {
- var billId = requestContext.pathParameters().getString("invoice").map(Bill.Id::of).orElseThrow(RestApiException.NotFound::new);
- var requestParam = requestContext.queryParameters().getString("keys").stream()
- .flatMap(s -> Arrays.stream(s.split(",")))
- .map(Bill.ItemKeyType::valueOf)
- .toList();
-
- var requestKeys = Bill.ItemRequest.of(requestParam);
- var bill = billing.getBill(billId);
- var response = bill.summarizeBy(requestKeys);
-
- var slime = new Slime();
- toSlime(slime.setObject(), bill, response, bill.lineItems().stream().filter(Bill.LineItem::isAdditional).toList());
- return slime;
- }
-
- private MessageResponse setAccountantTenantCollection(RestApi.RequestContext requestContext, Slime body) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
-
- var collection = CollectionMethod.valueOf(getInspectorFieldOrThrow(body.get(), "collection"));
- var result = billing.setCollectionMethod(tenant.name(), collection);
-
- if (result.isSuccess()) {
- return new MessageResponse("Collection: " + collection.name());
- } else {
- throw new RestApiException.BadRequest("Could not change collection method: " + result.getErrorMessage());
- }
- }
-
- private Slime accountantTenantCollection(RestApi.RequestContext requestContext) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
-
- var collection = billing.getCollectionMethod(tenant.name());
-
- var slime = new Slime();
- var root = slime.setObject();
- root.setString("collection", collection.name());
-
- return slime;
- }
-
- private Slime accountantTenant(RestApi.RequestContext requestContext) {
- var tenantName = TenantName.from(requestContext.pathParameters().getStringOrThrow("tenant"));
- var tenant = tenants.require(tenantName, CloudTenant.class);
-
- var slime = new Slime();
- var root = slime.setObject();
-
- var planId = billing.getPlan(tenant.name());
- var plan = planRegistry.plan(planId);
-
- var collection = billing.getCollectionMethod(tenant.name());
-
- toSlime(root, tenant, planId, plan, collection);
-
- return slime;
- }
-
- // --------- INVOICE RENDERING ----------
-
- private void invoicesSummaryToSlime(Cursor slime, List<Bill> bills) {
- bills.forEach(invoice -> invoiceSummaryToSlime(slime.addObject(), invoice));
- }
-
- private void invoiceSummaryToSlime(Cursor slime, Bill bill) {
- slime.setString("id", bill.id().value());
- slime.setString("from", bill.getStartDate().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("to", bill.getEndDate().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("total", bill.sum().toString());
- slime.setString("status", bill.status().value());
- }
-
- private void usageToSlime(Cursor slime, Bill bill) {
- slime.setString("from", bill.getStartDate().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("to", bill.getEndTime().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("total", bill.sum().toString());
- toSlime(slime.setArray("items"), bill.lineItems());
- }
-
- private void toSlime(Cursor slime, Bill bill) {
- slime.setString("id", bill.id().value());
- slime.setString("from", bill.getStartDate().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("to", bill.getEndDate().format(DateTimeFormatter.ISO_LOCAL_DATE));
- slime.setString("total", bill.sum().toString());
- slime.setString("status", bill.status().value());
- toSlime(slime.setArray("statusHistory"), bill.statusHistory());
- toSlime(slime.setArray("items"), bill.lineItems());
- }
-
- private void toSlime(Cursor slime, StatusHistory history) {
- history.getHistory().forEach((key, value) -> {
- var c = slime.addObject();
- c.setString("at", key.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
- c.setString("status", value.value());
- });
- }
-
- private void toSlime(Cursor slime, List<Bill.LineItem> items) {
- items.forEach(item -> toSlime(slime.addObject(), item));
- }
-
- private void toSlime(Cursor slime, Bill.LineItem item) {
- slime.setString("id", item.id());
- slime.setString("description", item.description());
- slime.setString("amount",item.amount().toString());
- toSlime(slime.setObject("plan"), planRegistry.plan(item.plan()).orElseThrow(() -> new RuntimeException("No such plan: '" + item.plan() + "'")));
- item.getArchitecture().ifPresent(arch -> slime.setString("architecture", arch.name()));
- slime.setLong("majorVersion", item.getMajorVersion());
- if (! item.getCloudAccount().isUnspecified())
- slime.setString("cloudAccount", item.getCloudAccount().value());
-
- item.applicationId().ifPresent(appId -> {
- slime.setString("application", appId.application().value());
- slime.setString("instance", appId.instance().value());
- });
-
- item.zoneId().ifPresent(z -> slime.setString("zone", z.value()));
-
- toSlime(slime.setObject("cpu"), item.getCpuHours(), item.getCpuCost());
- toSlime(slime.setObject("memory"), item.getMemoryHours(), item.getMemoryCost());
- toSlime(slime.setObject("disk"), item.getDiskHours(), item.getDiskCost());
- toSlime(slime.setObject("gpu"), item.getGpuHours(), item.getGpuCost());
- }
-
- private void toSlime(Cursor slime, Optional<BigDecimal> hours, Optional<BigDecimal> cost) {
- hours.ifPresent(h -> slime.setString("hours", h.toString()));
- cost.ifPresent(c -> slime.setString("cost", c.toString()));
- }
-
- private void toSlime(Cursor slime, CloudTenant tenant, PlanId planId, Optional<Plan> plan, CollectionMethod method) {
- slime.setString("tenant", tenant.name().value());
- toSlime(slime.setObject("plan"), planId, plan);
- toSlime(slime.setObject("billing"), tenant.billingReference());
- slime.setString("collection", method.name());
- }
-
- private void toSlime(Cursor slime, PlanId planId, Optional<Plan> plan) {
- slime.setString("id", planId.value());
- if (plan.isPresent()) {
- slime.setString("name", plan.get().displayName());
- slime.setBool("billed", plan.get().isBilled());
- slime.setBool("supported", plan.get().isSupported());
- } else {
- slime.setString("name", "UNKNOWN");
- slime.setBool("billed", false);
- slime.setBool("supported", false);
- }
- }
-
- private void toSlime(Cursor slime, Optional<BillingReference> billingReference) {
- if (billingReference.isPresent()) {
- slime.setString("id", billingReference.get().reference());
- slime.setLong("lastUpdated", billingReference.get().updated().toEpochMilli());
- }
- }
-
- private void toSlime(Cursor slime, Bill bill, Map<Bill.ItemKey, Bill.ItemSummary> summaries, List<Bill.LineItem> additional) {
- slime.setString("id", bill.id().value());
- var summaryCursor = slime.setArray("summary");
- summaries.forEach((key, summary) -> {
- toSlime(summaryCursor.addObject(), key, summary);
- });
- var additionalCursor = slime.setArray("additional");
- additional.forEach(item -> {
- additionalSummaryToSlime(additionalCursor, item);
- });
- }
-
- private void additionalSummaryToSlime(Cursor slime, Bill.LineItem item) {
- slime.setString("description", item.description());
- slime.setString("amount", item.amount().toPlainString());
- }
-
- private void toSlime(Cursor slime, Bill.ItemKey key, Bill.ItemSummary summary) {
- toSlime(slime.setObject("key"), key);
- toSlime(slime.setObject("summary"), summary);
- }
-
- private void toSlime(Cursor slime, Bill.ItemKey key) {
- key.keys().forEach((keyType, keyValue) -> {
- if (keyValue == null) slime.setNix(keyType.name());
- else slime.setString(keyType.name(), keyValue.toString());
- });
- }
-
- private void toSlime(Cursor slime, Bill.ItemSummary summary) {
- var cpu = slime.setObject("cpu");
- cpu.setString("cost", summary.cpuCost().toPlainString());
- cpu.setString("hours", summary.cpuUsage().toPlainString());
-
- var ram = slime.setObject("memory");
- ram.setString("cost", summary.ramCost().toPlainString());
- ram.setString("hours", summary.ramUsage().toPlainString());
-
- var disk = slime.setObject("disk");
- disk.setString("cost", summary.diskCost().toPlainString());
- disk.setString("hours", summary.diskUsage().toPlainString());
-
- var gpu = slime.setObject("gpu");
- gpu.setString("cost", summary.gpuCost().toPlainString());
- gpu.setString("hours", summary.gpuUsage().toPlainString());
- }
-
- private List<Object[]> toCsv(Bill bill) {
- return List.<Object[]>of(new Object[]{
- bill.id().value(), bill.tenant().value(),
- bill.getStartDate().format(DateTimeFormatter.ISO_DATE),
- bill.getEndDate().format(DateTimeFormatter.ISO_DATE),
- bill.sumCpuHours(), bill.sumMemoryHours(), bill.sumDiskHours(),
- bill.sumCpuCost(), bill.sumMemoryCost(), bill.sumDiskCost(),
- bill.sumAdditionalCost()
- });
- }
-
- // ---------- END INVOICE RENDERING ----------
-
- private LocalDate untilParameter(RestApi.RequestContext ctx) {
- return ctx.queryParameters().getString("until")
- .map(LocalDate::parse)
- .orElseGet(() -> LocalDate.now(clock));
- }
-
- private static String getInspectorFieldOrThrow(Inspector inspector, String field) {
- if (!inspector.field(field).valid())
- throw new RestApiException.BadRequest("Field " + field + " cannot be null");
- return inspector.field(field).asString();
- }
-
- private void toSlime(Cursor cursor, Plan plan) {
- cursor.setString("id", plan.id().value());
- cursor.setString("name", plan.displayName());
- }
-
- private void toSlime(Cursor cursor, Quota quota) {
- cursor.setDouble("budget", quota.budget().map(BigDecimal::doubleValue).orElse(-1.0));
- }
-
- private Plan planFor(TenantName tenant) {
- var planId = billing.getPlan(tenant);
- return planRegistry.plan(planId)
- .orElseThrow(() -> new RuntimeException("No such plan: '" + planId + "'"));
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/CsvResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/CsvResponse.java
deleted file mode 100644
index cf45bfb67f0..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/billing/CsvResponse.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.billing;
-
-import com.yahoo.container.jdisc.HttpResponse;
-import org.apache.commons.csv.CSVFormat;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.util.List;
-
-/**
- * @author ogronnesby
- */
-class CsvResponse extends HttpResponse {
-
- private final String[] header;
- private final List<Object[]> rows;
-
- CsvResponse(String[] header, List<Object[]> rows) {
- super(200);
- this.header = header;
- this.rows = rows;
- }
-
- @Override
- public void render(OutputStream outputStream) throws IOException {
- var writer = new OutputStreamWriter(outputStream);
- var printer = CSVFormat.DEFAULT.withRecordSeparator('\n').withHeader(this.header).print(writer);
- for (var row : this.rows) printer.printRecord(row);
- printer.flush();
- }
-
- @Override
- public String getContentType() {
- return "text/csv; encoding=utf-8";
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/certificate/EndpointCertificatesHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/certificate/EndpointCertificatesHandler.java
deleted file mode 100644
index b38bb73a98a..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/certificate/EndpointCertificatesHandler.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.certificate;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.RestApiException;
-import com.yahoo.restapi.StringResponse;
-import com.yahoo.vespa.flags.BooleanFlag;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.flags.StringFlag;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProvider;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateRequest;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.certificate.AssignedCertificate;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.persistence.EndpointCertificateSerializer;
-import com.yahoo.vespa.hosted.controller.routing.EndpointConfig;
-
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.Executor;
-import java.util.stream.Collectors;
-
-import static com.yahoo.jdisc.http.HttpRequest.Method.GET;
-import static com.yahoo.jdisc.http.HttpRequest.Method.POST;
-
-/**
- * List all certificate requests for a system, with their requested DNS names.
- * Used for debugging, and verifying basic functionality of Cameo client in CD.
- *
- * @author andreer
- */
-
-public class EndpointCertificatesHandler extends ThreadedHttpRequestHandler {
-
- private final EndpointCertificateProvider endpointCertificateProvider;
- private final CuratorDb curator;
- private final BooleanFlag useAlternateCertProvider;
- private final StringFlag endpointCertificateAlgo;
- private final Controller controller;
-
- public EndpointCertificatesHandler(Executor executor, ServiceRegistry serviceRegistry, CuratorDb curator, Controller controller) {
- super(executor);
- this.endpointCertificateProvider = serviceRegistry.endpointCertificateProvider();
- this.curator = curator;
- this.controller = controller;
- this.useAlternateCertProvider = PermanentFlags.USE_ALTERNATIVE_ENDPOINT_CERTIFICATE_PROVIDER.bindTo(controller.flagSource());
- this.endpointCertificateAlgo = PermanentFlags.ENDPOINT_CERTIFICATE_ALGORITHM.bindTo(controller.flagSource());
- }
-
- public HttpResponse handle(HttpRequest request) {
- if (request.getMethod().equals(GET)) return listEndpointCertificates();
- if (request.getMethod().equals(POST)) return reRequestEndpointCertificateFor(request.getProperty("application"), request.getProperty("ignoreExistingMetadata") != null);
- throw new RestApiException.MethodNotAllowed(request);
- }
-
- public HttpResponse listEndpointCertificates() {
- List<EndpointCertificateRequest> request = endpointCertificateProvider.listCertificates();
-
- String requestsWithNames = request.stream()
- .map(r -> r.requestId() + " : " +
- String.join(", ", r.dnsNames().stream()
- .map(EndpointCertificateRequest.DnsNameStatus::dnsName)
- .collect(Collectors.joining(", "))))
- .collect(Collectors.joining("\n"));
-
- return new StringResponse(requestsWithNames);
- }
-
- public StringResponse reRequestEndpointCertificateFor(String instanceId, boolean ignoreExisting) {
- ApplicationId applicationId = ApplicationId.fromFullString(instanceId);
- if (controller.routing().endpointConfig(applicationId) == EndpointConfig.generated) {
- throw new IllegalArgumentException("Cannot re-request certificate. " + instanceId + " is assigned certificate from a pool");
- }
- try (var lock = curator.lock(TenantAndApplicationId.from(applicationId))) {
- AssignedCertificate assignedCertificate = curator.readAssignedCertificate(TenantAndApplicationId.from(applicationId), Optional.of(applicationId.instance()))
- .orElseThrow(() -> new RestApiException.NotFound("No certificate found for application " + applicationId.serializedForm()));
-
- String algo = this.endpointCertificateAlgo.with(FetchVector.Dimension.INSTANCE_ID, applicationId.serializedForm()).value();
- boolean useAlternativeProvider = useAlternateCertProvider.with(FetchVector.Dimension.INSTANCE_ID, applicationId.serializedForm()).value();
- String keyPrefix = applicationId.toFullString();
-
- EndpointCertificate cert = endpointCertificateProvider.requestCaSignedCertificate(
- keyPrefix, assignedCertificate.certificate().requestedDnsSans(),
- ignoreExisting ?
- Optional.empty() :
- Optional.of(assignedCertificate.certificate()),
- algo, useAlternativeProvider);
-
- curator.writeAssignedCertificate(assignedCertificate.with(cert));
-
- return new StringResponse(EndpointCertificateSerializer.toSlime(cert).toString());
- }
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java
deleted file mode 100644
index f3b28691262..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandler.java
+++ /dev/null
@@ -1,307 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.changemanagement;
-
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.RestApiException;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
-import com.yahoo.vespa.hosted.controller.maintenance.ChangeManagementAssessor;
-import com.yahoo.vespa.hosted.controller.persistence.ChangeRequestSerializer;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.yolean.Exceptions;
-
-import java.io.IOException;
-import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-public class ChangeManagementApiHandler extends AuditLoggingRequestHandler {
-
- private final ChangeManagementAssessor assessor;
- private final Controller controller;
- private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneOffset.UTC);
-
- public ChangeManagementApiHandler(ThreadedHttpRequestHandler.Context ctx, Controller controller) {
- super(ctx, controller.auditLogger());
- this.assessor = new ChangeManagementAssessor(controller.serviceRegistry().configServer().nodeRepository());
- this.controller = controller;
- }
-
- @Override
- public HttpResponse auditAndHandle(HttpRequest request) {
- try {
- return switch (request.getMethod()) {
- case GET -> get(request);
- case POST -> post(request);
- case PATCH -> patch(request);
- case DELETE -> delete(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
- };
- } catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- } catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/changemanagement/v1/assessment/{changeRequestId}")) return changeRequestAssessment(path.get("changeRequestId"));
- if (path.matches("/changemanagement/v1/vcmr")) return getVCMRs();
- if (path.matches("/changemanagement/v1/vcmr/{vcmrId}")) return getVCMR(path.get("vcmrId"));
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse post(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/changemanagement/v1/assessment")) return doAssessment(request);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse patch(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/changemanagement/v1/vcmr/{vcmrId}")) return patchVCMR(request, path.get("vcmrId"));
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse delete(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/changemanagement/v1/vcmr/{vcmrId}")) return deleteVCMR(path.get("vcmrId"));
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private Inspector inspectorOrThrow(HttpRequest request) {
- try {
- return SlimeUtils.jsonToSlime(request.getData().readAllBytes()).get();
- } catch (IOException e) {
- throw new RestApiException.BadRequest("Failed to parse request body");
- }
- }
-
- private static Inspector getInspectorFieldOrThrow(Inspector inspector, String field) {
- if (!inspector.field(field).valid())
- throw new RestApiException.BadRequest("Field " + field + " cannot be null");
- return inspector.field(field);
- }
-
- private HttpResponse changeRequestAssessment(String changeRequestId) {
- var optionalChangeRequest = controller.curator().readChangeRequests()
- .stream()
- .filter(request -> changeRequestId.equals(request.getChangeRequestSource().id()))
- .findFirst();
-
- if (optionalChangeRequest.isEmpty())
- return ErrorResponse.notFoundError("Could not find any upcoming change requests with id " + changeRequestId);
-
- var changeRequest = optionalChangeRequest.get();
-
- return doAssessment(changeRequest.getImpactedHosts());
- }
-
- // The structure here should be
- //
- // {
- // hosts: string[]
- // switches: string[]
- // switchInSequence: boolean
- // }
- //
- // Only hosts is supported right now
- private HttpResponse doAssessment(HttpRequest request) {
-
- Inspector inspector = inspectorOrThrow(request);
-
- // For now; mandatory fields
- Inspector hostArray = inspector.field("hosts");
- Inspector switchArray = inspector.field("switches");
-
-
- // The impacted hostnames
- List<String> hostNames = new ArrayList<>();
- if (hostArray.valid()) {
- hostArray.traverse((ArrayTraverser) (i, host) -> hostNames.add(host.asString()));
- }
-
- if (switchArray.valid()) {
- List<String> switchNames = new ArrayList<>();
- switchArray.traverse((ArrayTraverser) (i, switchName) -> switchNames.add(switchName.asString()));
- hostNames.addAll(hostsOnSwitch(switchNames));
- }
-
- if (hostNames.isEmpty())
- return ErrorResponse.badRequest("No prod hosts in provided host/switch list");
-
- return doAssessment(hostNames);
- }
-
- private HttpResponse doAssessment(List<String> hostNames) {
- var zone = affectedZone(hostNames);
- if (zone.isEmpty())
- return ErrorResponse.notFoundError("Could not infer prod zone from host list: " + hostNames);
-
- ChangeManagementAssessor.Assessment assessments = assessor.assessment(hostNames, zone.get());
-
- Slime slime = new Slime();
- Cursor root = slime.setObject();
-
- // This is the main structure that might be part of something bigger later
- Cursor assessmentCursor = root.setObject("assessment");
-
- // Updated gives clue to if the assessment is old
- assessmentCursor.setString("updated", formatter.format(controller.clock().instant()));
-
- // Assessment on the cluster level
- Cursor clustersCursor = assessmentCursor.setArray("clusters");
-
- assessments.getClusterAssessments().forEach(assessment -> {
- Cursor oneCluster = clustersCursor.addObject();
- oneCluster.setString("app", assessment.app);
- oneCluster.setString("zone", assessment.zone);
- oneCluster.setString("cluster", assessment.cluster);
- oneCluster.setLong("clusterSize", assessment.clusterSize);
- oneCluster.setLong("clusterImpact", assessment.clusterImpact);
- oneCluster.setLong("groupsTotal", assessment.groupsTotal);
- oneCluster.setLong("groupsImpact", assessment.groupsImpact);
- oneCluster.setString("upgradePolicy", assessment.upgradePolicy);
- oneCluster.setString("suggestedAction", assessment.suggestedAction);
- oneCluster.setString("impact", assessment.impact);
- });
-
- Cursor hostsCursor = assessmentCursor.setArray("hosts");
- assessments.getHostAssessments().forEach(assessment -> {
- Cursor hostObject = hostsCursor.addObject();
- hostObject.setString("hostname", assessment.hostName);
- hostObject.setString("switchName", assessment.switchName);
- hostObject.setLong("numberOfChildren", assessment.numberOfChildren);
- hostObject.setLong("numberOfProblematicChildren", assessment.numberOfProblematicChildren);
- });
-
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse getVCMRs() {
- var changeRequests = controller.curator().readChangeRequests();
- var slime = new Slime();
- var cursor = slime.setObject().setArray("vcmrs");
- changeRequests.forEach(changeRequest -> {
- var changeCursor = cursor.addObject();
- ChangeRequestSerializer.writeChangeRequest(changeCursor, changeRequest);
- });
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse getVCMR(String vcmrId) {
- var changeRequest = controller.curator().readChangeRequest(vcmrId);
-
- if (changeRequest.isEmpty()) {
- return ErrorResponse.notFoundError("No VCMR with id: " + vcmrId);
- }
-
- var slime = new Slime();
- var cursor = slime.setObject();
-
- ChangeRequestSerializer.writeChangeRequest(cursor, changeRequest.get());
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse patchVCMR(HttpRequest request, String vcmrId) {
- var optionalChangeRequest = controller.curator().readChangeRequest(vcmrId);
-
- if (optionalChangeRequest.isEmpty()) {
- return ErrorResponse.notFoundError("No VCMR with id: " + vcmrId);
- }
-
- var changeRequest = optionalChangeRequest.get();
- var inspector = inspectorOrThrow(request);
-
- if (inspector.field("approval").valid()) {
- var approval = ChangeRequest.Approval.valueOf(inspector.field("approval").asString());
- changeRequest = changeRequest.withApproval(approval);
- }
-
- if (inspector.field("actionPlan").valid()) {
- var actionPlan = ChangeRequestSerializer.readHostActionPlan(inspector.field("actionPlan"));
- changeRequest = changeRequest.withActionPlan(actionPlan);
- }
-
- if (inspector.field("status").valid()) {
- var status = VespaChangeRequest.Status.valueOf(inspector.field("status").asString());
- changeRequest = changeRequest.withStatus(status);
- }
-
- try (var lock = controller.curator().lockChangeRequests()) {
- controller.curator().writeChangeRequest(changeRequest);
- }
-
- var slime = new Slime();
- var cursor = slime.setObject();
- ChangeRequestSerializer.writeChangeRequest(cursor, changeRequest);
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse deleteVCMR(String vcmrId) {
- var changeRequest = controller.curator().readChangeRequest(vcmrId);
-
- if (changeRequest.isEmpty()) {
- return ErrorResponse.notFoundError("No VCMR with id: " + vcmrId);
- }
-
- try (var lock = controller.curator().lockChangeRequests()) {
- controller.curator().deleteChangeRequest(changeRequest.get());
- }
-
- var slime = new Slime();
- var cursor = slime.setObject();
- ChangeRequestSerializer.writeChangeRequest(cursor, changeRequest.get());
- return new SlimeJsonResponse(slime);
- }
-
- private Optional<ZoneId> affectedZone(List<String> hosts) {
- NodeFilter affectedHosts = NodeFilter.all().hostnames(hosts.stream()
- .map(HostName::of)
- .collect(Collectors.toSet()));
- for (var zone : getProdZones()) {
- var affectedHostsInZone = controller.serviceRegistry().configServer().nodeRepository().list(zone, affectedHosts);
- if (!affectedHostsInZone.isEmpty())
- return Optional.of(zone);
- }
-
- return Optional.empty();
- }
-
- private List<String> hostsOnSwitch(List<String> switches) {
- return getProdZones().stream()
- .flatMap(zone -> controller.serviceRegistry().configServer().nodeRepository().list(zone, NodeFilter.all()).stream())
- .filter(node -> node.switchHostname().map(switches::contains).orElse(false))
- .map(node -> node.hostname().value())
- .toList();
- }
-
- private List<ZoneId> getProdZones() {
- return controller.zoneRegistry()
- .zones()
- .reachable()
- .in(Environment.prod)
- .ids();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java
deleted file mode 100644
index b1e44756802..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandler.java
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.configserver;
-
-import ai.vespa.http.HttpURL;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.config.provision.zone.ZoneList;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
-import com.yahoo.vespa.hosted.controller.proxy.ConfigServerRestExecutor;
-import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.yolean.Exceptions;
-
-import java.net.URI;
-import java.util.List;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static ai.vespa.http.HttpURL.Path.parse;
-
-/**
- * REST API for proxying operator APIs to config servers in a given zone.
- *
- * @author freva
- */
-@SuppressWarnings("unused")
-public class ConfigServerApiHandler extends AuditLoggingRequestHandler {
-
- private static final URI CONTROLLER_URI = URI.create("https://localhost:4443/");
- private static final List<HttpURL.Path> WHITELISTED_APIS = List.of(parse("/flags/v1/"),
- parse("/nodes/v2/"),
- parse("/orchestrator/v1/"),
- parse("/state/v1/"));
-
- private final ZoneRegistry zoneRegistry;
- private final ConfigServerRestExecutor proxy;
- private final ZoneId controllerZone;
-
- public ConfigServerApiHandler(Context parentCtx, ServiceRegistry serviceRegistry,
- ConfigServerRestExecutor proxy, Controller controller) {
- super(parentCtx, controller.auditLogger());
- this.zoneRegistry = serviceRegistry.zoneRegistry();
- this.controllerZone = zoneRegistry.systemZone().getVirtualId();
- this.proxy = proxy;
- }
-
- @Override
- public HttpResponse auditAndHandle(HttpRequest request) {
- try {
- return switch (request.getMethod()) {
- case GET -> get(request);
- case POST, PUT, DELETE, PATCH -> proxy(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
- };
- } catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- } catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/configserver/v1")) {
- return root(request);
- }
- return proxy(request);
- }
-
- private HttpResponse proxy(HttpRequest request) {
- Path path = new Path(request.getUri());
- if ( ! path.matches("/configserver/v1/{environment}/{region}/{*}")) {
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- ZoneId zoneId = ZoneId.from(path.get("environment"), path.get("region"));
- if ( ! zoneRegistry.hasZone(zoneId) && ! controllerZone.equals(zoneId)) {
- throw new IllegalArgumentException("No such zone: " + zoneId.value());
- }
-
- if (path.getRest().length() < 2 || ! WHITELISTED_APIS.contains(path.getRest().head(2).withTrailingSlash())) {
- return ErrorResponse.forbidden("Cannot access " + path.getRest() +
- " through /configserver/v1, following APIs are permitted: " + WHITELISTED_APIS.stream()
- .map(p -> "/" + String.join("/", p.segments()) + "/")
- .collect(Collectors.joining(", ")));
- }
-
- return proxy.handle(ProxyRequest.tryOne(getEndpoint(zoneId), path.getRest(), request));
- }
-
- private HttpResponse root(HttpRequest request) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- ZoneList zoneList = zoneRegistry.zones().reachable();
-
- Cursor zones = root.setArray("zones");
- Stream.concat(Stream.of(controllerZone), zoneRegistry.zones().reachable().ids().stream())
- .forEach(zone -> {
- Cursor object = zones.addObject();
- object.setString("environment", zone.environment().value());
- object.setString("region", zone.region().value());
- object.setString("uri", request.getUri().resolve(
- "/configserver/v1/" + zone.environment().value() + "/" + zone.region().value()).toString());
- });
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse notFound(Path path) {
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private URI getEndpoint(ZoneId zoneId) {
- return controllerZone.equals(zoneId) ? CONTROLLER_URI : zoneRegistry.getConfigServerVipUri(zoneId);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java
deleted file mode 100644
index 91dde82e233..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/configserver/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author freva
- */
-package com.yahoo.vespa.hosted.controller.restapi.configserver;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AccessRequestResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AccessRequestResponse.java
deleted file mode 100644
index 4863b91b3eb..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AccessRequestResponse.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-
-import java.util.Collection;
-
-public class AccessRequestResponse extends SlimeJsonResponse {
-
- public AccessRequestResponse(Collection<AthenzUser> members) {
- super(toSlime(members));
- }
-
- private static Slime toSlime(Collection<AthenzUser> members) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor array = root.setArray("members");
- members.stream()
- .map(AthenzIdentity::getFullName)
- .forEach(array::addString);
- return slime;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java
deleted file mode 100644
index 859281dbe18..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/AuditLogResponse.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLog;
-
-/**
- * @author mpolden
- */
-public class AuditLogResponse extends SlimeJsonResponse {
-
- public AuditLogResponse(AuditLog log) {
- super(toSlime(log));
- }
-
- private static Slime toSlime(AuditLog log) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor entryArray = root.setArray("entries");
- log.entries().forEach(entry -> {
- Cursor entryObject = entryArray.addObject();
- entryObject.setString("time", entry.at().toString());
- entryObject.setString("client", entry.client().name());
- entryObject.setString("user", entry.principal());
- entryObject.setString("method", entry.method().name());
- entryObject.setString("resource", entry.resource());
- entry.data().ifPresent(data -> entryObject.setString("data", data));
- });
- return slime;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java
deleted file mode 100644
index b9ba4f691fc..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiHandler.java
+++ /dev/null
@@ -1,200 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.container.jdisc.secretstore.SecretStore;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.MessageResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.ResourceResponse;
-import com.yahoo.restapi.RestApiException;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
-import com.yahoo.vespa.hosted.controller.config.CoreDumpTokenResealingConfig;
-import com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance;
-import com.yahoo.vespa.hosted.controller.maintenance.Upgrader;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccess;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-import com.yahoo.yolean.Exceptions;
-
-import java.io.InputStream;
-import java.security.Principal;
-import java.security.cert.X509Certificate;
-import java.time.Instant;
-import java.util.Optional;
-import java.util.Scanner;
-
-import static com.yahoo.vespa.hosted.controller.restapi.controller.RequestUtils.requireField;
-import static com.yahoo.vespa.hosted.controller.restapi.controller.RequestUtils.toJsonBytes;
-
-/**
- * This implements the controller/v1 API which provides operators with information about,
- * and control over the Controller.
- *
- * @author bratseth
- */
-@SuppressWarnings("unused") // Created by injection
-public class ControllerApiHandler extends AuditLoggingRequestHandler {
-
- private final ControllerMaintenance maintenance;
- private final Controller controller;
- private final SecretStore secretStore;
- private final CoreDumpTokenResealingConfig tokenResealingConfig;
-
- public ControllerApiHandler(ThreadedHttpRequestHandler.Context parentCtx,
- Controller controller,
- ControllerMaintenance maintenance,
- SecretStore secretStore,
- CoreDumpTokenResealingConfig tokenResealingConfig) {
- super(parentCtx, controller.auditLogger());
- this.controller = controller;
- this.maintenance = maintenance;
- this.secretStore = secretStore;
- this.tokenResealingConfig = tokenResealingConfig;
- }
-
- @Override
- public HttpResponse auditAndHandle(HttpRequest request) {
- try {
- return switch (request.getMethod()) {
- case GET -> get(request);
- case POST -> post(request);
- case DELETE -> delete(request);
- case PATCH -> patch(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
- };
- }
- catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- }
- catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/controller/v1/")) return root(request);
- if (path.matches("/controller/v1/auditlog/")) return new AuditLogResponse(controller.auditLogger().readLog());
- if (path.matches("/controller/v1/maintenance/")) return new JobsResponse(controller.jobControl());
- if (path.matches("/controller/v1/stats")) return new StatsResponse(controller);
- if (path.matches("/controller/v1/jobs/upgrader")) return new UpgraderResponse(maintenance.upgrader());
- if (path.matches("/controller/v1/metering/tenant/{tenant}/month/{month}")) return new MeteringResponse(controller.serviceRegistry().resourceDatabase(), path.get("tenant"), path.get("month"));
- return notFound(path);
- }
-
- private HttpResponse post(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/controller/v1/jobs/upgrader/confidence/{version}")) return overrideConfidence(request, path.get("version"));
- if (path.matches("/controller/v1/access/requests/{user}")) return approveMembership(request, path.get("user"));
- if (path.matches("/controller/v1/access/grants/{user}")) return grantAccess(request, path.get("user"));
- if (path.matches("/controller/v1/access/cores/reseal")) return DecryptionTokenResealer.handleResealRequest(request, tokenResealingConfig.resealingPrivateKeyName(), secretStore);
- return notFound(path);
- }
-
- private HttpResponse approveMembership(HttpRequest request, String user) {
- AthenzUser athenzUser = AthenzUser.fromUserId(user);
- byte[] jsonBytes = toJsonBytes(request.getData());
- Inspector inspector = SlimeUtils.jsonToSlime(jsonBytes).get();
- ApplicationId applicationId = requireField(inspector, "applicationId", ApplicationId::fromSerializedForm);
- ZoneId zone = requireField(inspector, "zone", ZoneId::from);
- if(controller.supportAccess().allowDataplaneMembership(athenzUser, new DeploymentId(applicationId, zone))) {
- return new AccessRequestResponse(controller.serviceRegistry().accessControlService().listMembers());
- } else {
- return new MessageResponse(400, "Unable to approve membership request");
- }
- }
-
- private HttpResponse grantAccess(HttpRequest request, String user) {
- Principal principal = requireUserPrincipal(request);
- Instant now = controller.clock().instant();
-
- byte[] jsonBytes = toJsonBytes(request.getData());
- Inspector requestObject = SlimeUtils.jsonToSlime(jsonBytes).get();
- X509Certificate certificate = requireField(requestObject, "certificate", X509CertificateUtils::fromPem);
- ApplicationId applicationId = requireField(requestObject, "applicationId", ApplicationId::fromSerializedForm);
- ZoneId zone = requireField(requestObject, "zone", ZoneId::from);
- DeploymentId deployment = new DeploymentId(applicationId, zone);
-
- // Register grant
- SupportAccess supportAccess = controller.supportAccess().registerGrant(deployment, principal.getName(), certificate);
-
- // Trigger deployment to include operator cert
- Optional<JobId> jobId = controller.applications().deploymentTrigger().reTriggerOrAddToQueue(deployment, "re-triggered to grant access, by " + request.getJDiscRequest().getUserPrincipal().getName());
- return new MessageResponse(
- jobId.map(id -> Text.format("Operator %s granted access and job %s triggered", principal.getName(), id.type().jobName()))
- .orElseGet(() -> Text.format("Operator %s granted access and job trigger queued", principal.getName())));
- }
-
- private HttpResponse delete(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/controller/v1/jobs/upgrader/confidence/{version}")) return removeConfidenceOverride(path.get("version"));
- return notFound(path);
- }
-
- private HttpResponse patch(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/controller/v1/jobs/upgrader")) return configureUpgrader(request);
- return notFound(path);
- }
-
- private HttpResponse notFound(Path path) { return ErrorResponse.notFoundError("Nothing at " + path); }
-
- private HttpResponse root(HttpRequest request) {
- return new ResourceResponse(request, "auditlog", "maintenance", "stats", "jobs/upgrader", "metering/tenant");
- }
-
- private HttpResponse configureUpgrader(HttpRequest request) {
- String upgradesPerMinuteField = "upgradesPerMinute";
-
- byte[] jsonBytes = toJsonBytes(request.getData());
- Inspector inspect = SlimeUtils.jsonToSlime(jsonBytes).get();
- Upgrader upgrader = maintenance.upgrader();
-
- if (inspect.field(upgradesPerMinuteField).valid()) {
- upgrader.setUpgradesPerMinute(inspect.field(upgradesPerMinuteField).asDouble());
- } else {
- return ErrorResponse.badRequest("No such modifiable field(s)");
- }
-
- return new UpgraderResponse(maintenance.upgrader());
- }
-
- private HttpResponse removeConfidenceOverride(String version) {
- maintenance.upgrader().removeConfidenceOverride(Version.fromString(version));
- return new UpgraderResponse(maintenance.upgrader());
- }
-
- private HttpResponse overrideConfidence(HttpRequest request, String version) {
- Confidence confidence = Confidence.valueOf(asString(request.getData()).trim());
- maintenance.upgrader().overrideConfidence(Version.fromString(version), confidence);
- return new UpgraderResponse(maintenance.upgrader());
- }
-
- private static String asString(InputStream in) {
- Scanner scanner = new Scanner(in).useDelimiter("\\A");
- if (scanner.hasNext()) {
- return scanner.next();
- }
- return "";
- }
-
- private static Principal requireUserPrincipal(HttpRequest request) {
- Principal principal = request.getJDiscRequest().getUserPrincipal();
- if (principal == null) throw new RestApiException.InternalServerError("Expected a user principal");
- return principal;
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/DecryptionTokenResealer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/DecryptionTokenResealer.java
deleted file mode 100644
index f2e51b51752..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/DecryptionTokenResealer.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.secretstore.SecretStore;
-import com.yahoo.security.KeyId;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.security.SharedKeyResealingSession;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.SlimeUtils;
-
-import java.util.Optional;
-
-import static com.yahoo.vespa.hosted.controller.restapi.controller.RequestUtils.requireField;
-import static com.yahoo.vespa.hosted.controller.restapi.controller.RequestUtils.toJsonBytes;
-
-/**
- * @author vekterli
- */
-class DecryptionTokenResealer {
-
- private static int checkKeyNameAndExtractVersion(KeyId tokenKeyId, String expectedKeyName) {
- String keyStr = tokenKeyId.asString();
- int versionSepIdx = keyStr.lastIndexOf('.');
- if (versionSepIdx == -1) {
- throw new IllegalArgumentException("Key ID is not of the form 'name.version'");
- }
- String keyName = keyStr.substring(0, versionSepIdx);
- if (!expectedKeyName.equals(keyName)) {
- throw new IllegalArgumentException("Token is not generated for the expected key");
- }
- int keyVersion;
- try {
- keyVersion = Integer.parseInt(keyStr.substring(versionSepIdx + 1));
- } catch (IllegalArgumentException e) {
- throw new IllegalArgumentException("Key version is not a valid integer");
- }
- if (keyVersion < 0) {
- throw new IllegalArgumentException("Key version is out of range");
- }
- return keyVersion;
- }
-
- /**
- * Extracts a resealing requests from an <strong>already authenticated</strong> HTTP request
- * and re-seals it towards the requested public key, using the provided private key name to
- * decrypt the token contained in the request.
- *
- * @param request a request with a JSON payload that contains a resealing request.
- * @param privateKeyName The key name used to look up the decryption secret.
- * The token must have a matching key name, or the request will be rejected.
- * @param secretStore SecretStore instance that holds the private key. The request will fail otherwise.
- * @return a response with a JSON payload containing a resealing response (any failure will throw).
- */
- static HttpResponse handleResealRequest(HttpRequest request, String privateKeyName, SecretStore secretStore) {
- if (privateKeyName.isEmpty()) {
- throw new IllegalArgumentException("Private key ID is not set");
- }
- byte[] jsonBytes = toJsonBytes(request.getData());
- var inspector = SlimeUtils.jsonToSlime(jsonBytes).get();
- var resealRequest = requireField(inspector, "resealRequest", SharedKeyResealingSession.ResealingRequest::fromSerializedString);
- int keyVersion = checkKeyNameAndExtractVersion(resealRequest.sealedKey().keyId(), privateKeyName);
-
- var b58EncodedPrivateKey = secretStore.getSecret(privateKeyName, keyVersion);
- if (b58EncodedPrivateKey == null) {
- throw new IllegalArgumentException("Unknown key ID or version");
- }
- var privateKey = KeyUtils.fromBase58EncodedX25519PrivateKey(b58EncodedPrivateKey);
- var resealResponse = SharedKeyResealingSession.reseal(resealRequest, (keyId) -> Optional.of(privateKey));
- return new ResealedTokenResponse(resealResponse);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/JobsResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/JobsResponse.java
deleted file mode 100644
index 0d15d9b2971..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/JobsResponse.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.concurrent.maintenance.JobControl;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-
-import java.util.TreeSet;
-
-/**
- * A response containing maintenance job status
- *
- * @author bratseth
- */
-public class JobsResponse extends SlimeJsonResponse {
-
- public JobsResponse(JobControl jobControl) {
- super(toSlime(jobControl));
- }
-
- private static Slime toSlime(JobControl jobControl) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
-
- Cursor jobArray = root.setArray("jobs");
- for (String jobName : jobControl.jobs())
- jobArray.addObject().setString("name", jobName);
-
- Cursor inactiveArray = root.setArray("inactive");
- for (String jobName : new TreeSet<>(jobControl.inactiveJobs()))
- inactiveArray.addString(jobName);
-
- return slime;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/MeteringResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/MeteringResponse.java
deleted file mode 100644
index 5a8c4847ce6..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/MeteringResponse.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceSnapshot;
-
-import java.time.YearMonth;
-import java.util.List;
-
-/**
- * @author olaa
- */
-public class MeteringResponse extends SlimeJsonResponse {
-
- public MeteringResponse(ResourceDatabaseClient resourceClient, String tenantName, String month) {
- super(toSlime(resourceClient, tenantName, month));
- }
-
- private static Slime toSlime(ResourceDatabaseClient resourceClient, String tenantName, String month) {
- Slime slime = new Slime();
- Cursor root = slime.setArray();
- List<ResourceSnapshot> snapshots = resourceClient.getRawSnapshotHistoryForTenant(TenantName.from(tenantName), YearMonth.parse(month));
- snapshots.forEach(snapshot -> {
- Cursor object = root.addObject();
- object.setString("applicationId", snapshot.getApplicationId().toFullString());
- object.setLong("timestamp", snapshot.getTimestamp().toEpochMilli());
- object.setString("zoneId", snapshot.getZoneId().value());
- object.setDouble("cpu", snapshot.resources().vcpu());
- object.setDouble("memory", snapshot.resources().memoryGb());
- object.setDouble("disk", snapshot.resources().diskGb());
- object.setString("architecture", snapshot.resources().architecture().name());
- object.setLong("version", snapshot.getMajorVersion());
- object.setDouble("gpuMemoryGb", snapshot.resources().gpuResources().memoryGb());
- object.setLong("gpuCount", snapshot.resources().gpuResources().count());
- });
- return slime;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/RequestUtils.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/RequestUtils.java
deleted file mode 100644
index 746f1d8ce2e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/RequestUtils.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.io.IOUtils;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.SlimeUtils;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.util.function.Function;
-
-class RequestUtils {
-
- static <T> T requireField(Inspector inspector, String field, Function<String, T> mapper) {
- return SlimeUtils.optionalString(inspector.field(field))
- .map(mapper::apply)
- .orElseThrow(() -> new IllegalArgumentException("Expected field \"" + field + "\" in request"));
- }
-
- static byte[] toJsonBytes(InputStream jsonStream) {
- try {
- return IOUtils.readBytes(jsonStream, 1000 * 1000);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ResealedTokenResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ResealedTokenResponse.java
deleted file mode 100644
index 2aab64a7c30..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/ResealedTokenResponse.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.security.SharedKeyResealingSession;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-
-/**
- * A response that contains a decryption token "resealing response".
- *
- * @author vekterli
- */
-public class ResealedTokenResponse extends SlimeJsonResponse {
-
- public ResealedTokenResponse(SharedKeyResealingSession.ResealingResponse response) {
- super(toSlime(response));
- }
-
- private static Slime toSlime(SharedKeyResealingSession.ResealingResponse response) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- root.setString("resealResponse", response.toSerializedString());
- return slime;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/StatsResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/StatsResponse.java
deleted file mode 100644
index ab12187c069..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/StatsResponse.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationStats;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Load;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepoStats;
-
-/**
- * A response containing statistics about this controller and its zones.
- *
- * @author bratseth
- */
-public class StatsResponse extends SlimeJsonResponse {
-
- public StatsResponse(Controller controller) {
- super(toSlime(controller));
- }
-
- private static Slime toSlime(Controller controller) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor zonesArray = root.setArray("zones");
- for (ZoneId zone : controller.zoneRegistry().zones().reachable().ids()) {
- NodeRepoStats stats = controller.serviceRegistry().configServer().nodeRepository().getStats(zone);
- if (stats.applicationStats().isEmpty()) continue; // skip empty zones
- Cursor zoneObject = zonesArray.addObject();
- zoneObject.setString("id", zone.toString());
- zoneObject.setDouble("totalCost", stats.totalCost());
- zoneObject.setDouble("totalAllocatedCost", stats.totalAllocatedCost());
- toSlime(stats.load(), zoneObject.setObject("load"));
- toSlime(stats.activeLoad(), zoneObject.setObject("activeLoad"));
- Cursor applicationsArray = zoneObject.setArray("applications");
- for (var applicationStats : stats.applicationStats())
- toSlime(applicationStats, applicationsArray.addObject());
- }
- return slime;
- }
-
- private static void toSlime(ApplicationStats stats, Cursor applicationObject) {
- applicationObject.setString("id", stats.id().toFullString());
- toSlime(stats.load(), applicationObject.setObject("load"));
- applicationObject.setDouble("cost", stats.cost());
- applicationObject.setDouble("unutilizedCost", stats.unutilizedCost());
- }
-
- private static void toSlime(Load load, Cursor loadObject) {
- loadObject.setDouble("cpu", load.cpu());
- loadObject.setDouble("memory", load.memory());
- loadObject.setDouble("disk", load.disk());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java
deleted file mode 100644
index e8ba1177c67..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/UpgraderResponse.java
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.maintenance.Upgrader;
-
-/**
- * @author mpolden
- */
-public class UpgraderResponse extends SlimeJsonResponse {
-
- public UpgraderResponse(Upgrader upgrader) {
- super(toSlime(upgrader));
- }
-
- private static Slime toSlime(Upgrader upgrader) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- root.setDouble("upgradesPerMinute", upgrader.upgradesPerMinute());
-
- Cursor array = root.setArray("confidenceOverrides");
- upgrader.confidenceOverrides().forEach((version, confidence) -> {
- Cursor object = array.addObject();
- object.setString(version.toFullString(), confidence.name());
- });
-
- return slime;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandler.java
deleted file mode 100644
index 63f600aaa50..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandler.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.StringResponse;
-import com.yahoo.vespa.hosted.controller.config.WellKnownFolderConfig;
-import com.yahoo.yolean.Exceptions;
-
-
-/**
- * Responsible for serving contents from the RFC 8615 well-known directory
- * @author olaa
- */
-public class WellKnownApiHandler extends ThreadedHttpRequestHandler {
-
- private final String securityTxt;
-
- public WellKnownApiHandler(Context context, WellKnownFolderConfig wellKnownFolderConfig) {
- super(context);
- this.securityTxt = wellKnownFolderConfig.securityTxt();
- }
-
- @Override
- public HttpResponse handle(HttpRequest request) {
- return switch (request.getMethod()) {
- case GET -> get(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
- };
- }
-
- private HttpResponse get(HttpRequest request) {
- try {
- Path path = new Path(request.getUri());
- if (path.matches("/.well-known/security.txt")) return securityTxt();
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
- catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- }
- }
-
- private HttpResponse securityTxt() {
- return new StringResponse(securityTxt);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java
deleted file mode 100644
index 834133e7eb5..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenService.java
+++ /dev/null
@@ -1,261 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.dataplanetoken;
-
-import com.yahoo.concurrent.DaemonThreadFactory;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.security.token.Token;
-import com.yahoo.security.token.TokenCheckHash;
-import com.yahoo.security.token.TokenDomain;
-import com.yahoo.security.token.TokenGenerator;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneToken;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions.Version;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.FingerPrint;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-
-import java.security.Principal;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Phaser;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static java.util.Comparator.comparing;
-import static java.util.Comparator.naturalOrder;
-import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.toMap;
-
-/**
- * Service to list, generate and delete data plane tokens
- *
- * @author mortent
- */
-public class DataplaneTokenService {
-
- private static final String TOKEN_PREFIX = "vespa_cloud_";
- private static final int TOKEN_BYTES = 32;
- private static final int CHECK_HASH_BYTES = 32;
- public static final Duration DEFAULT_TTL = Duration.ofDays(30);
-
- private final ExecutorService executor = Executors.newCachedThreadPool(new DaemonThreadFactory("dataplane-token-service-"));
- private final Controller controller;
-
- public DataplaneTokenService(Controller controller) {
- this.controller = controller;
- }
-
- /**
- * List valid tokens for a tenant
- */
- public List<DataplaneTokenVersions> listTokens(TenantName tenantName) {
- return controller.curator().readDataplaneTokens(tenantName);
- }
-
- public enum State { UNUSED, DEPLOYING, ACTIVE, REVOKING }
-
- /** List all known tokens for a tenant, with the state of each token version (both current and deactivating). */
- public Map<DataplaneTokenVersions, Map<FingerPrint, State>> listTokensWithState(TenantName tenantName) {
- List<DataplaneTokenVersions> currentTokens = listTokens(tenantName);
- Set<TokenId> usedTokens = new HashSet<>();
- Map<HostName, Map<TokenId, List<FingerPrint>>> activeTokens = listActiveTokens(tenantName, usedTokens);
- Map<TokenId, Map<FingerPrint, Boolean>> activeFingerprints = computeStates(activeTokens);
- Map<DataplaneTokenVersions, Map<FingerPrint, State>> tokens = new TreeMap<>(comparing(DataplaneTokenVersions::tokenId));
- for (DataplaneTokenVersions token : currentTokens) {
- Map<FingerPrint, State> states = new TreeMap<>();
- // Current tokens are active iff. they are active everywhere.
- for (Version version : token.tokenVersions()) {
- // If the token was not seen anywhere, it is deploying or unused.
- // Otherwise, it is active iff. it is active everywhere.
- Boolean isActive = activeFingerprints.getOrDefault(token.tokenId(), Map.of()).get(version.fingerPrint());
- states.put(version.fingerPrint(),
- isActive == null ? usedTokens.contains(token.tokenId()) ? State.DEPLOYING : State.UNUSED
- : isActive ? State.ACTIVE : State.DEPLOYING);
- }
- // Active, non-current token versions are deactivating.
- for (FingerPrint print : activeFingerprints.getOrDefault(token.tokenId(), Map.of()).keySet()) {
- states.putIfAbsent(print, State.REVOKING);
- }
- tokens.put(token, states);
- }
- // Active, non-current tokens are also deactivating.
- activeFingerprints.forEach((id, prints) -> {
- if (currentTokens.stream().noneMatch(token -> token.tokenId().equals(id))) {
- Map<FingerPrint, State> states = new TreeMap<>();
- for (FingerPrint print : prints.keySet()) states.put(print, State.REVOKING);
- tokens.put(new DataplaneTokenVersions(id, List.of(), Instant.EPOCH), states);
- }
- });
- return tokens;
- }
-
- private Map<HostName, Map<TokenId, List<FingerPrint>>> listActiveTokens(TenantName tenantName, Set<TokenId> usedTokens) {
- Map<HostName, Map<TokenId, List<FingerPrint>>> tokens = new ConcurrentHashMap<>();
- Phaser phaser = new Phaser(1);
- for (Application application : controller.applications().asList(tenantName)) {
- for (Instance instance : application.instances().values()) {
- instance.deployments().forEach((zone, deployment) -> {
- DeploymentId id = new DeploymentId(instance.id(), zone);
- usedTokens.addAll(deployment.dataPlaneTokens().keySet());
- phaser.register();
- executor.execute(() -> {
- try { tokens.putAll(controller.serviceRegistry().configServer().activeTokenFingerprints(id)); }
- finally { phaser.arrive(); }
- });
- });
- }
- }
- phaser.arriveAndAwaitAdvance();
- return tokens;
- }
-
- /** Computes whether each print is active on all hosts where its token is present. */
- private Map<TokenId, Map<FingerPrint, Boolean>> computeStates(Map<HostName, Map<TokenId, List<FingerPrint>>> activeTokens) {
- Map<TokenId, Map<FingerPrint, Boolean>> states = new HashMap<>();
- for (Map<TokenId, List<FingerPrint>> token : activeTokens.values()) {
- token.forEach((id, prints) -> {
- states.merge(id,
- prints.stream().collect(toMap(print -> print, __ -> true)),
- (a, b) -> new HashMap<>() {{ // true iff. present in both, false iff. present in one.
- a.forEach((p, s) -> put(p, s && b.getOrDefault(p, false)));
- b.forEach((p, s) -> putIfAbsent(p, false));
- }});
- });
- }
- return states;
- }
-
- /** Triggers redeployment of all applications which reference a token which has changed. */
- public void triggerTokenChangeDeployments() {
- controller.applications().asList().stream()
- .collect(groupingBy(application -> application.id().tenant()))
- .forEach((tenant, applications) -> {
- List<DataplaneTokenVersions> currentTokens = listTokens(tenant);
- for (Application application : applications) {
- for (Instance instance : application.instances().values()) {
- instance.deployments().forEach((zone, deployment) -> {
- if (zone.environment().isTest()) return;
- if (deployment.dataPlaneTokens().isEmpty()) return;
- boolean needsRetrigger = false;
- // If a token has a newer change than the deployed token data, we need to re-trigger.
- for (DataplaneTokenVersions token : currentTokens)
- needsRetrigger |= deployment.dataPlaneTokens().getOrDefault(token.tokenId(), Instant.MAX).isBefore(token.lastUpdated());
-
- // If a token is no longer current, but was deployed with at least one version, we need to re-trigger.
- for (var entry : deployment.dataPlaneTokens().entrySet())
- needsRetrigger |= ! Instant.EPOCH.equals(entry.getValue())
- && currentTokens.stream().noneMatch(token -> token.tokenId().equals(entry.getKey()));
-
- if (needsRetrigger && controller.jobController().last(instance.id(), JobType.deploymentTo(zone)).map(Run::hasEnded).orElse(true))
- controller.applications().deploymentTrigger().reTrigger(instance.id(),
- JobType.deploymentTo(zone),
- "Data plane tokens changed");
- });
- }
- }
- });
- }
-
- /**
- * Generates a token using tenant name as the check access context.
- * Persists the token fingerprint and check access hash, but not the token value
- *
- * @param tenantName name of the tenant to connect the token to
- * @param tokenId The user generated name/id of the token
- * @param expiration Token expiration
- * @param principal The principal making the request
- * @return a DataplaneToken containing the secret generated token
- */
- public DataplaneToken generateToken(TenantName tenantName, TokenId tokenId, Instant expiration, Principal principal) {
- TokenDomain tokenDomain = TokenDomain.of("Vespa Cloud tenant data plane:%s".formatted(tenantName.value()));
- Token token = TokenGenerator.generateToken(tokenDomain, TOKEN_PREFIX, TOKEN_BYTES);
- TokenCheckHash checkHash = TokenCheckHash.of(token, CHECK_HASH_BYTES);
- Instant now = controller.clock().instant();
- DataplaneTokenVersions.Version newTokenVersion = new DataplaneTokenVersions.Version(
- FingerPrint.of(token.fingerprint().toDelimitedHexString()),
- checkHash.toHexString(),
- now,
- Optional.ofNullable(expiration),
- principal.getName());
-
- CuratorDb curator = controller.curator();
- try (Mutex lock = curator.lock(tenantName)) {
- List<DataplaneTokenVersions> dataplaneTokenVersions = curator.readDataplaneTokens(tenantName);
- Optional<DataplaneTokenVersions> existingToken = dataplaneTokenVersions.stream().filter(t -> Objects.equals(t.tokenId(), tokenId)).findFirst();
- if (existingToken.isPresent()) {
- List<DataplaneTokenVersions.Version> versions = existingToken.get().tokenVersions();
- versions = Stream.concat(
- versions.stream(),
- Stream.of(newTokenVersion))
- .toList();
- dataplaneTokenVersions = Stream.concat(
- dataplaneTokenVersions.stream().filter(t -> !Objects.equals(t.tokenId(), tokenId)),
- Stream.of(new DataplaneTokenVersions(tokenId, versions, now)))
- .toList();
- } else {
- DataplaneTokenVersions newToken = new DataplaneTokenVersions(tokenId, List.of(newTokenVersion), now);
- dataplaneTokenVersions = Stream.concat(dataplaneTokenVersions.stream(), Stream.of(newToken)).toList();
- }
- curator.writeDataplaneTokens(tenantName, dataplaneTokenVersions);
- }
-
- // Return the data plane token including the secret token.
- return new DataplaneToken(tokenId, FingerPrint.of(token.fingerprint().toDelimitedHexString()),
- token.secretTokenString(), Optional.ofNullable(expiration));
- }
-
- /**
- * Deletes the token version identitfied by tokenId and tokenFingerPrint
- * @throws IllegalArgumentException if the version could not be found
- */
- public void deleteToken(TenantName tenantName, TokenId tokenId, FingerPrint tokenFingerprint) {
- CuratorDb curator = controller.curator();
- try (Mutex lock = curator.lock(tenantName)) {
- List<DataplaneTokenVersions> dataplaneTokenVersions = curator.readDataplaneTokens(tenantName);
- Optional<DataplaneTokenVersions> existingToken = dataplaneTokenVersions.stream().filter(t -> Objects.equals(t.tokenId(), tokenId)).findFirst();
- if (existingToken.isPresent()) {
- List<DataplaneTokenVersions.Version> versions = existingToken.get().tokenVersions();
- versions = versions.stream().filter(v -> !Objects.equals(v.fingerPrint(), tokenFingerprint)).toList();
- if (versions.isEmpty()) {
- dataplaneTokenVersions = dataplaneTokenVersions.stream().filter(t -> !Objects.equals(t.tokenId(), tokenId)).toList();
- } else {
- Optional<Version> existingVersion = existingToken.get().tokenVersions().stream().filter(v -> v.fingerPrint().equals(tokenFingerprint)).findAny();
- if (existingVersion.isPresent()) {
- Instant now = controller.clock().instant();
- // If we removed an expired token, we keep the old lastUpdated timestamp.
- Instant lastUpdated = existingVersion.get().expiration().map(now::isAfter).orElse(false) ? existingToken.get().lastUpdated() : now;
- dataplaneTokenVersions = Stream.concat(dataplaneTokenVersions.stream().filter(t -> !Objects.equals(t.tokenId(), tokenId)),
- Stream.of(new DataplaneTokenVersions(tokenId, versions, lastUpdated))).toList();
- } else {
- throw new IllegalArgumentException("Fingerprint does not exist: " + tokenFingerprint);
- }
- }
- curator.writeDataplaneTokens(tenantName, dataplaneTokenVersions);
- } else {
- throw new IllegalArgumentException("Token does not exist: " + tokenId);
- }
- }
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java
deleted file mode 100644
index 839dbf76faa..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiHandler.java
+++ /dev/null
@@ -1,163 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.deployment;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.container.jdisc.EmptyResponse;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.jdisc.http.HttpRequest.Method;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus;
-import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.yolean.Exceptions;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.time.Instant;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-import java.util.logging.Logger;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * This API serves redirects to a badge server.
- *
- * @author jonmv
- */
-@SuppressWarnings("unused") // Handler
-public class BadgeApiHandler extends ThreadedHttpRequestHandler {
-
- private final static Logger log = Logger.getLogger(BadgeApiHandler.class.getName());
-
- private final Controller controller;
- private final Map<Key, Value> badgeCache = new ConcurrentHashMap<>();
-
- public BadgeApiHandler(Context parentCtx, Controller controller) {
- super(parentCtx);
- this.controller = controller;
- }
-
- @Override
- public HttpResponse handle(HttpRequest request) {
- Method method = request.getMethod();
- try {
- return switch (method) {
- case OPTIONS -> new SvgHttpResponse("") {{
- headers().add("Allow", "GET, HEAD, OPTIONS");
- headers().add("Access-Control-Allow-Origin", "*");
- headers().add("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
- }};
- case HEAD, GET -> get(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + method + "' is unsupported");
- };
- } catch (IllegalArgumentException|IllegalStateException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- } catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/badge/v1/{tenant}/{application}/{instance}")) return overviewBadge(path.get("tenant"), path.get("application"), path.get("instance"));
- if (path.matches("/badge/v1/{tenant}/{application}/{instance}/{jobName}")) return historyBadge(path.get("tenant"), path.get("application"), path.get("instance"), path.get("jobName"), request.getProperty("historyLength"));
-
- return ErrorResponse.notFoundError(Text.format("No '%s' handler at '%s'", request.getMethod(),
- request.getUri().getPath()));
- }
-
- /** Returns a URI which points to an overview badge for the given application. */
- private HttpResponse overviewBadge(String tenant, String application, String instance) {
- ApplicationId id = ApplicationId.from(tenant, application, instance);
- return cachedResponse(new Key(id, null, 0),
- controller.clock().instant(),
- () -> {
- DeploymentStatus status = controller.jobController().deploymentStatus(controller.applications().requireApplication(TenantAndApplicationId.from(id)));
- Predicate<JobStatus> isDeclaredJob = job -> status.jobSteps().get(job.id()) != null && status.jobSteps().get(job.id()).isDeclared();
- return Badges.overviewBadge(id, status.jobs().instance(id.instance()).matching(isDeclaredJob));
- });
- }
-
- /** Returns a URI which points to a history badge for the given application and job type. */
- private HttpResponse historyBadge(String tenant, String application, String instance, String jobName, String historyLength) {
- ApplicationId id = ApplicationId.from(tenant, application, instance);
- JobType type = JobType.fromJobName(jobName, controller.zoneRegistry());
- int length = historyLength == null ? 5 : Math.min(32, Math.max(0, Integer.parseInt(historyLength)));
- return cachedResponse(new Key(id, type, length),
- controller.clock().instant(),
- () -> Badges.historyBadge(id,
- controller.jobController().jobStatus(new JobId(id, type)),
- length)
- );
- }
-
- private HttpResponse cachedResponse(Key key, Instant now, Supplier<String> badge) {
- return new SvgHttpResponse(badgeCache.compute(key, (__, value) -> {
- return value != null && value.expiry.isAfter(now) ? value : new Value(badge.get(), now);
- }).badgeSvg);
- }
-
- private static class SvgHttpResponse extends HttpResponse {
- private final String svg;
- SvgHttpResponse(String svg) { super(200); this.svg = svg; }
- @Override public void render(OutputStream outputStream) throws IOException {
- outputStream.write(svg.getBytes(UTF_8));
- }
- @Override public String getContentType() {
- return "image/svg+xml";
- }
- }
-
-
- private static class Key {
-
- private final ApplicationId id;
- private final JobType type;
- private final int historyLength;
-
- private Key(ApplicationId id, JobType type, int historyLength) {
- this.id = id;
- this.type = type;
- this.historyLength = historyLength;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Key key = (Key) o;
- return historyLength == key.historyLength && id.equals(key.id) && Objects.equals(type, key.type);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id, type, historyLength);
- }
-
- }
-
- private static class Value {
-
- private final String badgeSvg;
- private final Instant expiry;
-
- private Value(String badgeSvg, Instant created) {
- this.badgeSvg = badgeSvg;
- this.expiry = created.plusSeconds(60);
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java
deleted file mode 100644
index 41b5c833ec8..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/Badges.java
+++ /dev/null
@@ -1,310 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.deployment;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.slime.ArrayTraverser;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.deployment.JobList;
-import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-
-public class Badges {
-
- // https://chrishewett.com/blog/calculating-text-width-programmatically/ thank you!
- private static final String characterWidths = "[[\" \",35.156],[\"!\",39.355],[\"\\\"\",45.898],[\"#\",81.836],[\"$\",63.574],[\"%\",107.617],[\"&\",72.656],[\"'\",26.855],[\"(\",45.41],[\")\",45.41],[\"*\",63.574],[\"+\",81.836],[\",\",36.377],[\"-\",45.41],[\".\",36.377],[\"/\",45.41],[\"0\",63.574],[\"1\",63.574],[\"2\",63.574],[\"3\",63.574],[\"4\",63.574],[\"5\",63.574],[\"6\",63.574],[\"7\",63.574],[\"8\",63.574],[\"9\",63.574],[\":\",45.41],[\";\",45.41],[\"<\",81.836],[\"=\",81.836],[\">\",81.836],[\"?\",54.541],[\"@\",100],[\"A\",68.359],[\"B\",68.555],[\"C\",69.824],[\"D\",77.051],[\"E\",63.232],[\"F\",57.471],[\"G\",77.539],[\"H\",75.146],[\"I\",42.09],[\"J\",45.459],[\"K\",69.287],[\"L\",55.664],[\"M\",84.277],[\"N\",74.805],[\"O\",78.711],[\"P\",60.303],[\"Q\",78.711],[\"R\",69.531],[\"S\",68.359],[\"T\",61.621],[\"U\",73.193],[\"V\",68.359],[\"W\",98.877],[\"X\",68.506],[\"Y\",61.523],[\"Z\",68.506],[\"[\",45.41],[\"\\\\\",45.41],[\"]\",45.41],[\"^\",81.836],[\"_\",63.574],[\"`\",63.574],[\"a\",60.059],[\"b\",62.305],[\"c\",52.1],[\"d\",62.305],[\"e\",59.57],[\"f\",35.156],[\"g\",62.305],[\"h\",63.281],[\"i\",27.441],[\"j\",34.424],[\"k\",59.18],[\"l\",27.441],[\"m\",97.266],[\"n\",63.281],[\"o\",60.693],[\"p\",62.305],[\"q\",62.305],[\"r\",42.676],[\"s\",52.1],[\"t\",39.404],[\"u\",63.281],[\"v\",59.18],[\"w\",81.836],[\"x\",59.18],[\"y\",59.18],[\"z\",52.539],[\"{\",63.477],[\"|\",45.41],[\"}\",63.477],[\"~\",81.836],[\"_median\",63.281]]";
- private static final double[] widths = new double[128]; // 0-94 hold widths for corresponding chars (+32); 95 holds the fallback width.
-
- static {
- SlimeUtils.jsonToSlimeOrThrow(characterWidths).get()
- .traverse((ArrayTraverser) (i, pair) -> {
- if (i < 95)
- assert Arrays.equals(new byte[]{(byte) (i + 32)}, pair.entry(0).asUtf8()) : i + ": " + pair.entry(0).asString();
- else
- assert "_median".equals(pair.entry(0).asString());
-
- widths[i] = pair.entry(1).asDouble();
- });
- }
-
- /** Character pixel width of a 100px size Verdana font rendering of the given code point, for code points in the range [32, 126]. */
- public static double widthOf(int codePoint) {
- return 32 <= codePoint && codePoint <= 126 ? widths[codePoint - 32] : widths[95];
- }
-
- /** Computes an approximate pixel width of the given size Verdana font rendering of the given string, ignoring kerning. */
- public static double widthOf(String text, int size) {
- return text.codePoints().mapToDouble(Badges::widthOf).sum() * (size - 0.5) / 100;
- }
-
- /** Computes an approximate pixel width of a 11px size Verdana font rendering of the given string, ignoring kerning. */
- public static double widthOf(String text) {
- return widthOf(text, 11);
- }
-
- static String colorOf(Run run, Optional<RunStatus> previous) {
- return switch (run.status()) {
- case running -> switch (previous.orElse(RunStatus.success)) {
- case success -> "url(#run-on-success)";
- case cancelled, aborted, noTests -> "url(#run-on-warning)";
- default -> "url(#run-on-failure)";
- };
- case success -> success;
- case cancelled, aborted, noTests -> warning;
- default -> failure;
- };
- }
-
- static String nameOf(JobType type) {
- return type.isTest() ? type.isProduction() ? "test"
- : type.jobName()
- : type.jobName().replace("production-", "");
- }
-
- static final double xPad = 6;
- static final double logoSize = 16;
- static final String dark = "#404040";
- static final String success = "#00f844";
- static final String running = "#ab83ff";
- static final String failure = "#bf103c";
- static final String warning = "#bd890b";
-
- static void addText(List<String> texts, String text, double x, double width) {
- addText(texts, text, x, width, 11);
- }
-
- static void addText(List<String> texts, String text, double x, double width, int size) {
- texts.add(" <text font-size='" + size + "' x='" + (x + 0.5) + "' y='" + (15) + "' fill='#000' fill-opacity='.4' textLength='" + width + "'>" + text + "</text>\n");
- texts.add(" <text font-size='" + size + "' x='" + x + "' y='" + (14) + "' fill='#fff' textLength='" + width + "'>" + text + "</text>\n");
- }
-
- static void addShade(List<String> sections, double x, double width) {
- sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + (width + 6) + "' height='20' fill='url(#shade)'/>\n");
- }
-
- static void addShadow(List<String> sections, double x) {
- sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + 8 + "' height='20' fill='url(#shadow)'/>\n");
- }
-
- static String historyBadge(ApplicationId id, JobStatus status, int length) {
- List<String> sections = new ArrayList<>();
- List<String> texts = new ArrayList<>();
-
- double x = 0;
- String text = id.toFullString();
- double textWidth = widthOf(text);
- double dx = xPad + logoSize + xPad + textWidth + xPad;
-
- addShade(sections, x, dx);
- sections.add(" <rect width='" + dx + "' height='20' fill='" + dark + "'/>\n");
- addText(texts, text, x + (xPad + logoSize + dx) / 2, textWidth);
- x += dx;
-
- if (status.lastTriggered().isEmpty())
- return badge(sections, texts, x);
-
- Run lastTriggered = status.lastTriggered().get();
- List<Run> runs = status.runs().descendingMap().values().stream()
- .filter(Run::hasEnded)
- .skip(1)
- .limit(length)
- .toList();
-
- text = lastTriggered.id().type().jobName();
- textWidth = widthOf(text);
- dx = xPad + textWidth + xPad;
- addShade(sections, x, dx);
- sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + (dx + 6) + "' height='20' fill='" + colorOf(lastTriggered, status.lastStatus()) + "'/>\n");
- addShadow(sections, x + dx);
- addText(texts, text, x + dx / 2, textWidth);
- x += dx;
-
- dx = xPad * (192.0 / (32 + runs.size())); // Broader sections with shorter history.
- for (Run run : runs) {
- addShade(sections, x, dx);
- sections.add(" <rect x='" + (x - 6) + "' rx='3' width='" + (dx + 6) + "' height='20' fill='" + colorOf(run, Optional.empty()) + "'/>\n");
- addShadow(sections, x + dx);
- dx *= Math.pow(0.3, 1.0 / (runs.size() + 8)); // Gradually narrowing sections with age.
- x += dx;
- }
- Collections.reverse(sections);
-
- return badge(sections, texts, x);
- }
-
- static String overviewBadge(ApplicationId id, JobList jobs) {
- // Put production tests right after their deployments, for a more compact rendering.
- List<Run> runs = new ArrayList<>(jobs.lastTriggered().asList());
- boolean anyTest = false;
- for (int i = 0; i < runs.size(); i++) {
- Run run = runs.get(i);
- if (run.id().type().isProduction() && run.id().type().isTest()) {
- anyTest = true;
- int j = i;
- while ( ! runs.get(j - 1).id().type().zone().equals(run.id().type().zone()))
- runs.set(j, runs.get(--j));
- runs.set(j, run);
- }
- }
-
- List<String> sections = new ArrayList<>();
- List<String> texts = new ArrayList<>();
-
- double x = 0;
- String text = id.toFullString();
- double textWidth = widthOf(text);
- double dx = xPad + logoSize + xPad + textWidth + xPad;
- double tdx = xPad + widthOf("test");
-
- addShade(sections, 0, dx);
- sections.add(" <rect width='" + dx + "' height='20' fill='" + dark + "'/>\n");
- addText(texts, text, x + (xPad + logoSize + dx) / 2, textWidth);
- x += dx;
-
- for (int i = 0; i < runs.size(); i++) {
- Run run = runs.get(i);
- Run test = i + 1 < runs.size() ? runs.get(i + 1) : null;
- if (test == null || ! test.id().type().isTest() || ! test.id().type().isProduction())
- test = null;
-
- boolean isTest = run.id().type().isTest() && run.id().type().isProduction();
- text = nameOf(run.id().type());
- textWidth = widthOf(text, isTest ? 9 : 11);
- dx = xPad + textWidth + (isTest ? 0 : xPad);
- Optional<RunStatus> previous = jobs.get(run.id().job()).flatMap(JobStatus::lastStatus);
-
- addText(texts, text, x + (dx - (isTest ? xPad : 0)) / 2, textWidth, isTest ? 9 : 11);
-
- // Add "deploy" when appropriate
- if ( ! run.id().type().isTest() && anyTest) {
- String deploy = "deploy";
- textWidth = widthOf(deploy, 9);
- addText(texts, deploy, x + dx + textWidth / 2, textWidth, 9);
- dx += textWidth + xPad;
- }
-
- // Add shade across zone section.
- if ( ! (isTest))
- addShade(sections, x, dx + (test != null ? tdx : 0));
-
- // Add colored section for job ...
- if (test == null)
- sections.add(" <rect x='" + (x - 16) + "' rx='3' width='" + (dx + 16) + "' height='20' fill='" + colorOf(run, previous) + "'/>\n");
- // ... with a slant if a test is next.
- else
- sections.add(" <polygon points='" + (x - 6) + " 0 " + (x - 6) + " 20 " + (x + dx - 7) + " 20 " + (x + dx + 1) + " 0' fill='" + colorOf(run, previous) + "'/>\n");
-
- // Cast a shadow onto the next zone ...
- if (test == null)
- addShadow(sections, x + dx);
-
- x += dx;
- }
- Collections.reverse(sections);
-
- return badge(sections, texts, x);
- }
-
- static String badge(List<String> sections, List<String> texts, double width) {
- return "<svg xmlns='http://www.w3.org/2000/svg' width='" + width + "' height='20' role='img' aria-label='Deployment Status'>\n" +
- " <title>Deployment Status</title>\n" +
- // Lighting to give the badge a 3d look--dispersion at the top, shadow at the bottom.
- " <linearGradient id='light' x2='0' y2='100%'>\n" +
- " <stop offset='0' stop-color='#fff' stop-opacity='.5'/>\n" +
- " <stop offset='.1' stop-color='#fff' stop-opacity='.15'/>\n" +
- " <stop offset='.9' stop-color='#000' stop-opacity='.15'/>\n" +
- " <stop offset='1' stop-color='#000' stop-opacity='.5'/>\n" +
- " </linearGradient>\n" +
- // Dispersed light at the left of the badge.
- " <linearGradient id='left-light' x2='100%' y2='0'>\n" +
- " <stop offset='0' stop-color='#fff' stop-opacity='.3'/>\n" +
- " <stop offset='.5' stop-color='#fff' stop-opacity='.1'/>\n" +
- " <stop offset='1' stop-color='#fff' stop-opacity='.0'/>\n" +
- " </linearGradient>\n" +
- // Shadow at the right of the badge.
- " <linearGradient id='right-shadow' x2='100%' y2='0'>\n" +
- " <stop offset='0' stop-color='#000' stop-opacity='.0'/>\n" +
- " <stop offset='.5' stop-color='#000' stop-opacity='.1'/>\n" +
- " <stop offset='1' stop-color='#000' stop-opacity='.3'/>\n" +
- " </linearGradient>\n" +
- // Shadow to highlight the border between sections, without using a heavy separator.
- " <linearGradient id='shadow' x2='100%' y2='0'>\n" +
- " <stop offset='0' stop-color='#222' stop-opacity='.3'/>\n" +
- " <stop offset='.625' stop-color='#555' stop-opacity='.3'/>\n" +
- " <stop offset='.9' stop-color='#555' stop-opacity='.05'/>\n" +
- " <stop offset='1' stop-color='#555' stop-opacity='.0'/>\n" +
- " </linearGradient>\n" +
- // Weak shade across each panel to highlight borders further.
- " <linearGradient id='shade' x2='100%' y2='0'>\n" +
- " <stop offset='0' stop-color='#000' stop-opacity='.20'/>\n" +
- " <stop offset='0.05' stop-color='#000' stop-opacity='.10'/>\n" +
- " <stop offset='1' stop-color='#000' stop-opacity='.0'/>\n" +
- " </linearGradient>\n" +
- // Running color sloshing back and forth on top of the failure color.
- " <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'>\n" +
- " <stop offset='0' stop-color='" + running + "' />\n" +
- " <stop offset='1' stop-color='" + failure + "' />\n" +
- " <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />\n" +
- " <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />\n" +
- " </linearGradient>\n" +
- // Running color sloshing back and forth on top of the warning color.
- " <linearGradient id='run-on-warning' x1='40%' x2='80%' y2='0%'>\n" +
- " <stop offset='0' stop-color='" + running + "' />\n" +
- " <stop offset='1' stop-color='" + warning + "' />\n" +
- " <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />\n" +
- " <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />\n" +
- " </linearGradient>\n" +
- // Running color sloshing back and forth on top of the success color.
- " <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'>\n" +
- " <stop offset='0' stop-color='" + running + "' />\n" +
- " <stop offset='1' stop-color='" + success + "' />\n" +
- " <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />\n" +
- " <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />\n" +
- " </linearGradient>\n" +
- // Clipping to give the badge rounded corners.
- " <clipPath id='rounded'>\n" +
- " <rect width='" + width + "' height='20' rx='3' fill='#fff'/>\n" +
- " </clipPath>\n" +
- // Badge section backgrounds with status colors and shades for distinction.
- " <g clip-path='url(#rounded)'>\n" +
- String.join("", sections) +
- " <rect width='" + 2 + "' height='20' fill='url(#left-light)'/>\n" +
- " <rect x='" + (width - 2) + "' width='" + 2 + "' height='20' fill='url(#right-shadow)'/>\n" +
- " <rect width='" + width + "' height='20' fill='url(#light)'/>\n" +
- " </g>\n" +
- " <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='11'>\n" +
- // The vespa.ai logo (with a slightly coloured shadow)!
- " <svg x='" + (xPad + 0.5) + "' y='" + ((20 - logoSize) / 2 + 1) + "' width='" + logoSize + "' height='" + logoSize + "' viewBox='0 0 150 150'>\n" +
- " <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>\n" +
- " <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>\n" +
- " <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>\n" +
- " <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>\n" +
- " </svg>\n" +
- " <svg x='" + xPad + "' y='" + ((20 - logoSize) / 2) + "' width='" + logoSize + "' height='" + logoSize + "' viewBox='0 0 150 150'>\n" +
- " <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'>\n" +
- " <stop offset='0.01' stop-color='#c6783e'/>\n" +
- " <stop offset='0.54' stop-color='#ff9750'/>\n" +
- " </linearGradient>\n" +
- " <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'>\n" +
- " <stop offset='0' stop-color='#005a8e'/>\n" +
- " <stop offset='0.54' stop-color='#1a7db6'/>\n" +
- " </linearGradient>\n" +
- " <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>\n" +
- " <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>\n" +
- " <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>\n" +
- " <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>\n" +
- " </svg>\n" +
- // Application ID and job names.
- String.join("", texts) +
- " </g>\n" +
- "</svg>\n";
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/CliApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/CliApiHandler.java
deleted file mode 100644
index 150acd297c2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/CliApiHandler.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.yolean.Exceptions;
-
-/**
- * This handler implements the /cli/v1/ API. The API allows Vespa CLI to retrieve information about the system, without
- * authorization. One example of such information is the minimum Vespa CLI version supported by our APIs.
- *
- * @author mpolden
- */
-public class CliApiHandler extends ThreadedHttpRequestHandler {
-
- /**
- * The minimum version of Vespa CLI which is considered compatible with our APIs. If a version of Vespa CLI below
- * this version tries to use our APIs, Vespa CLI will print a warning instructing the user to upgrade.
- */
- private static final Version MIN_CLI_VERSION = Version.fromString("7.547.18");
-
- public CliApiHandler(Context context) {
- super(context);
- }
-
- @Override
- public HttpResponse handle(HttpRequest request) {
- try {
- return switch (request.getMethod()) {
- case GET -> get(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
- };
- }
- catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- }
- catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/cli/v1/")) return root();
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse root() {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- root.setString("minVersion", MIN_CLI_VERSION.toFullString());
- return new SlimeJsonResponse(slime);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
deleted file mode 100644
index edfa4d01d78..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiHandler.java
+++ /dev/null
@@ -1,300 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentInstanceSpec;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.container.jdisc.EmptyResponse;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.restapi.UriBuilder;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus.DelayCause;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus.Readiness;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Versions;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.vespa.hosted.controller.versions.DeploymentStatistics;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.yolean.Exceptions;
-
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.TreeMap;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static java.util.function.Function.identity;
-import static java.util.stream.Collectors.groupingBy;
-import static java.util.stream.Collectors.toList;
-import static java.util.stream.Collectors.toMap;
-import static java.util.stream.Collectors.toUnmodifiableMap;
-
-/**
- * This implements the deployment/v1 API which provides information about the status of Vespa platform and
- * application deployments.
- *
- * @author bratseth
- */
-@SuppressWarnings("unused") // Injected
-public class DeploymentApiHandler extends ThreadedHttpRequestHandler {
-
- private final Controller controller;
-
- public DeploymentApiHandler(ThreadedHttpRequestHandler.Context parentCtx, Controller controller) {
- super(parentCtx);
- this.controller = controller;
- }
-
- @Override
- public HttpResponse handle(HttpRequest request) {
- try {
- return switch (request.getMethod()) {
- case GET -> handleGET(request);
- case OPTIONS -> handleOPTIONS();
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
- };
- }
- catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- }
- catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse handleGET(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/deployment/v1/")) return root(request);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse handleOPTIONS() {
- // We implement this to avoid redirect loops on OPTIONS requests from browsers, but do not really bother
- // spelling out the methods supported at each path, which we should
- EmptyResponse response = new EmptyResponse();
- response.headers().put("Allow", "GET,OPTIONS");
- return response;
- }
-
- private HttpResponse root(HttpRequest request) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor platformArray = root.setArray("versions");
- var versionStatus = controller.readVersionStatus();
- ApplicationList applications = ApplicationList.from(controller.applications().asList()).withJobs();
- var deploymentStatuses = controller.jobController().deploymentStatuses(applications, versionStatus);
- Map<Version, DeploymentStatistics> deploymentStatistics = DeploymentStatistics.compute(versionStatus.versions().stream().map(VespaVersion::versionNumber).toList(),
- deploymentStatuses)
- .stream().collect(toMap(DeploymentStatistics::version, identity()));
- for (VespaVersion version : versionStatus.versions()) {
- Cursor versionObject = platformArray.addObject();
- versionObject.setString("version", version.versionNumber().toString());
- versionObject.setString("confidence", version.confidence().name());
- versionObject.setString("commit", version.releaseCommit());
- versionObject.setLong("date", version.committedAt().toEpochMilli());
- versionObject.setBool("controllerVersion", version.isControllerVersion());
- versionObject.setBool("systemVersion", version.isSystemVersion());
-
- Cursor configServerArray = versionObject.setArray("configServers");
- for (var nodeVersion : version.nodeVersions()) {
- Cursor configServerObject = configServerArray.addObject();
- configServerObject.setString("hostname", nodeVersion.hostname().value());
- }
-
- DeploymentStatistics statistics = deploymentStatistics.get(version.versionNumber());
- Cursor failingArray = versionObject.setArray("failingApplications");
- for (Run run : statistics.failingUpgrades()) {
- Cursor applicationObject = failingArray.addObject();
- toSlime(applicationObject, run.id().application(), request);
- applicationObject.setString("failing", run.id().type().jobName());
- applicationObject.setString("status", nameOf(run.status()));
- }
-
- var statusByInstance = deploymentStatuses.asList().stream()
- .flatMap(status -> status.instanceJobs().keySet().stream()
- .map(instance -> Map.entry(instance, status)))
- .collect(toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue));
- var jobsByInstance = statusByInstance.entrySet().stream()
- .collect(toUnmodifiableMap(Map.Entry::getKey,
- entry -> entry.getValue().instanceJobs().get(entry.getKey())));
- Cursor productionArray = versionObject.setArray("productionApplications");
- statistics.productionSuccesses().stream()
- .collect(groupingBy(run -> run.id().application(), TreeMap::new, toList()))
- .forEach((id, runs) -> {
- Cursor applicationObject = productionArray.addObject();
- toSlime(applicationObject, id, request);
- applicationObject.setLong("productionJobs", jobsByInstance.get(id).production().size());
- applicationObject.setLong("productionSuccesses", runs.size());
- });
-
- Cursor runningArray = versionObject.setArray("deployingApplications");
- for (Run run : statistics.runningUpgrade()) {
- Cursor applicationObject = runningArray.addObject();
- toSlime(applicationObject, run.id().application(), request);
- applicationObject.setString("running", run.id().type().jobName());
- }
-
- Cursor instancesArray = versionObject.setArray("applications");
- Stream.of(statistics.failingUpgrades().stream().map(run -> new RunInfo(run, true)),
- statistics.otherFailing().stream().map(run -> new RunInfo(run, false)),
- statistics.runningUpgrade().stream().map(run -> new RunInfo(run, true)),
- statistics.otherRunning().stream().map(run -> new RunInfo(run, false)),
- statistics.productionSuccesses().stream().map(run -> new RunInfo(run, true)))
- .flatMap(identity())
- .collect(Collectors.groupingBy(run -> run.run.id().application(),
- LinkedHashMap::new, // Put apps with failing and running jobs first.
- groupingBy(run -> run.run.id().type(),
- LinkedHashMap::new,
- toList())))
- .forEach((instance, runs) -> {
- var status = statusByInstance.get(instance);
- var jobsToRun = status.jobsToRun();
- Cursor instanceObject = instancesArray.addObject();
- instanceObject.setString("tenant", instance.tenant().value());
- instanceObject.setString("application", instance.application().value());
- instanceObject.setString("instance", instance.instance().value());
- instanceObject.setBool("upgrading", status.application().require(instance.instance()).change().platform().equals(Optional.of(statistics.version())));
- instanceObject.setBool("pinned", status.application().require(instance.instance()).change().isPlatformPinned());
- instanceObject.setBool("platformPinned", status.application().require(instance.instance()).change().isPlatformPinned());
- instanceObject.setBool("revisionPinned", status.application().require(instance.instance()).change().isRevisionPinned());
- DeploymentStatus.StepStatus stepStatus = status.instanceSteps().get(instance.instance());
- if (stepStatus != null) { // Instance may not have any steps, i.e. an empty deployment spec has been submitted
- Readiness platformReadiness = stepStatus.blockedUntil(Change.of(statistics.version()));
- if (platformReadiness.cause() == DelayCause.changeBlocked)
- instanceObject.setLong("blockedUntil", platformReadiness.at().toEpochMilli());
- }
- instanceObject.setString("upgradePolicy", toString(status.application().deploymentSpec().instance(instance.instance())
- .map(DeploymentInstanceSpec::upgradePolicy)
- .orElse(DeploymentSpec.UpgradePolicy.defaultPolicy)));
- status.application().revisions().last().flatMap(ApplicationVersion::compileVersion)
- .ifPresent(compiled -> instanceObject.setString("compileVersion", compiled.toFullString()));
- Cursor jobsArray = instanceObject.setArray("jobs");
- status.jobSteps().forEach((job, jobStatus) -> {
- if ( ! job.application().equals(instance)) return;
- Cursor jobObject = jobsArray.addObject();
- jobObject.setString("name", job.type().jobName());
- if (jobsToRun.containsKey(job)) {
- Readiness readiness = jobsToRun.get(job).get(0).readiness();
- switch (readiness.cause()) {
- case paused -> jobObject.setLong("pausedUntil", readiness.at().toEpochMilli());
- case coolingDown -> jobObject.setLong("coolingDownUntil", readiness.at().toEpochMilli());
- }
- List<Versions> versionsOnThisPlatform = jobsToRun.get(job).stream()
- .map(DeploymentStatus.Job::versions)
- .filter(versions -> versions.targetPlatform().equals(statistics.version()))
- .toList();
- if ( ! versionsOnThisPlatform.isEmpty())
- jobObject.setString("pending", versionsOnThisPlatform.stream()
- .allMatch(versions -> versions.sourcePlatform()
- .map(statistics.version()::equals)
- .orElse(true))
- ? "application" : "platform");
- }
- });
- Cursor allRunsObject = instanceObject.setObject("allRuns");
- Cursor upgradeRunsObject = instanceObject.setObject("upgradeRuns");
- runs.forEach((type, rs) -> {
- Cursor runObject = allRunsObject.setObject(type.jobName());
- Cursor upgradeObject = upgradeRunsObject.setObject(type.jobName());
- CloudAccount cloudAccount = controller.applications().decideCloudAccountOf(new DeploymentId(instance, type.zone()),
- status.application().deploymentSpec())
- .orElse(null);
- for (RunInfo run : rs) {
- toSlime(runObject, run.run, cloudAccount);
- if (run.upgrade)
- toSlime(upgradeObject, run.run, cloudAccount);
- }
- });
- });
- }
- JobType.allIn(controller.zoneRegistry()).stream()
- .filter(job -> ! job.environment().isManuallyDeployed())
- .map(JobType::jobName).forEach(root.setArray("jobs")::addString);
- return new SlimeJsonResponse(slime);
- }
-
- private void toSlime(Cursor jobObject, Run run, CloudAccount cloudAccount) {
- String key = run.hasFailed() ? "failing" : run.hasEnded() ? "success" : "running";
- Cursor runObject = jobObject.setObject(key);
- runObject.setLong("number", run.id().number());
- runObject.setLong("start", run.start().toEpochMilli());
- run.end().ifPresent(end -> runObject.setLong("end", end.toEpochMilli()));
- runObject.setString("status", nameOf(run.status()));
- if (cloudAccount != null) runObject.setObject("enclave").setString("cloudAccount", cloudAccount.value());
- }
-
- private void toSlime(Cursor object, ApplicationId id, HttpRequest request) {
- object.setString("tenant", id.tenant().value());
- object.setString("application", id.application().value());
- object.setString("instance", id.instance().value());
- object.setString("url", new UriBuilder(request.getUri()).withPath("/application/v4/tenant/" +
- id.tenant().value() +
- "/application/" +
- id.application().value()).toString());
- object.setString("upgradePolicy", toString(controller.applications().requireApplication(TenantAndApplicationId.from(id))
- .deploymentSpec().instance(id.instance()).map(DeploymentInstanceSpec::upgradePolicy)
- .orElse(DeploymentSpec.UpgradePolicy.defaultPolicy)));
- }
-
- private static String toString(DeploymentSpec.UpgradePolicy upgradePolicy) {
- if (upgradePolicy == DeploymentSpec.UpgradePolicy.defaultPolicy) {
- return "default";
- }
- return upgradePolicy.name();
- }
-
- public static String nameOf(RunStatus status) {
- return switch (status) {
- case reset, running -> "running";
- case cancelled, aborted -> "aborted";
- case error -> "error";
- case testFailure -> "testFailure";
- case noTests -> "noTests";
- case endpointCertificateTimeout -> "endpointCertificateTimeout";
- case nodeAllocationFailure -> "nodeAllocationFailure";
- case installationFailed -> "installationFailed";
- case invalidApplication, deploymentFailed -> "deploymentFailed";
- case success -> "success";
- case quotaExceeded -> "quotaExceeded";
- };
- }
-
- private static class RunInfo {
- final Run run;
- final boolean upgrade;
-
- RunInfo(Run run, boolean upgrade) {
- this.run = run;
- this.upgrade = upgrade;
- }
-
- @Override
- public String toString() {
- return run.id().toString();
- }
-
- }
-
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java
deleted file mode 100644
index 0a466b7ffe8..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilter.java
+++ /dev/null
@@ -1,226 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.filter;
-
-import com.auth0.jwt.JWT;
-import com.auth0.jwt.interfaces.DecodedJWT;
-import com.auth0.jwt.interfaces.Payload;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.jdisc.Response;
-import com.yahoo.jdisc.http.filter.DiscFilterRequest;
-import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
-import com.yahoo.restapi.Path;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.athenz.client.zms.ZmsClientException;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.TenantController;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactory;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
-import com.yahoo.vespa.hosted.controller.security.Credentials;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.yolean.Exceptions;
-
-import java.net.URI;
-import java.security.cert.X509Certificate;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities.SCREWDRIVER_DOMAIN;
-
-/**
- * Enriches the request principal with roles from Athenz, if an AthenzPrincipal is set on the request.
- *
- * @author jonmv
- */
-public class AthenzRoleFilter extends JsonSecurityRequestFilterBase {
-
- private static final Logger logger = Logger.getLogger(AthenzRoleFilter.class.getName());
-
- private final AthenzFacade athenz;
- private final TenantController tenants;
- private final ExecutorService executor;
- private final ZoneRegistry zones;
-
- @Inject
- public AthenzRoleFilter(AthenzClientFactory athenzClientFactory, Controller controller) {
- this.athenz = new AthenzFacade(athenzClientFactory);
- this.tenants = controller.tenants();
- this.executor = Executors.newCachedThreadPool();
- this.zones = controller.zoneRegistry();
- }
-
- @Override
- protected Optional<ErrorResponse> filter(DiscFilterRequest request) {
- try {
- if (request.getUserPrincipal() instanceof AthenzPrincipal principal) {
- Optional<DecodedJWT> oktaAt = Optional.ofNullable((String) request.getAttribute("okta.access-token")).map(JWT::decode);
- Optional<X509Certificate> cert = request.getClientCertificateChain().stream().findFirst();
- Instant issuedAt = cert.map(X509Certificate::getNotBefore)
- .or(() -> oktaAt.map(Payload::getIssuedAt))
- .map(Date::toInstant).orElse(Instant.EPOCH);
- Instant expireAt = cert.map(X509Certificate::getNotAfter)
- .or(() -> oktaAt.map(Payload::getExpiresAt))
- .map(Date::toInstant).orElse(Instant.MAX);
- request.setAttribute(SecurityContext.ATTRIBUTE_NAME,
- new SecurityContext(principal, roles(principal, request.getUri()), issuedAt, expireAt));
- }
- }
- catch (Exception e) {
- logger.log(Level.INFO, () -> "Exception mapping Athenz principal to roles: " + Exceptions.toMessageString(e));
- return Optional.of(new ErrorResponse(Response.Status.FORBIDDEN, "Access denied"));
- }
- return Optional.empty();
- }
-
- Set<Role> roles(AthenzPrincipal principal, URI uri) throws Exception {
- Path path = new Path(uri);
-
- path.matches("/application/v4/tenant/{tenant}/{*}");
- Optional<Tenant> tenant = Optional.ofNullable(path.get("tenant")).map(TenantName::from).flatMap(tenants::get);
-
- path.matches("/application/v4/tenant/{tenant}/application/{application}/{*}");
- Optional<ApplicationName> application = Optional.ofNullable(path.get("application")).map(ApplicationName::from);
-
- final Optional<ZoneId> zone;
- if (path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}/{*}")) {
- zone = Optional.of(ZoneId.from(path.get("environment"), path.get("region")));
- } else if(path.matches("/application/v4/tenant/{tenant}/application/{application}/environment/{environment}/region/{region}/{*}")) {
- zone = Optional.of(ZoneId.from(path.get("environment"), path.get("region")));
- } else if(path.matches("/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/deploy/{jobname}")) {
- zone = Optional.of(JobType.fromJobName(path.get("jobname"), zones).zone());
- } else {
- zone = Optional.empty();
- }
-
- AthenzIdentity identity = principal.getIdentity();
-
- Set<Role> roleMemberships = new CopyOnWriteArraySet<>();
- List<Future<?>> futures = new ArrayList<>();
-
- futures.add(executor.submit(() -> {
- if (athenz.hasHostedOperatorAccess(identity))
- roleMemberships.add(Role.hostedOperator());
- }));
-
- futures.add(executor.submit(() -> {
- if (athenz.hasHostedSupporterAccess(identity))
- roleMemberships.add(Role.hostedSupporter());
- }));
-
- futures.add(executor.submit(() -> {
- // Add all tenants that are accessible for this request
- athenz.accessibleTenants(tenants.asList(), new Credentials(principal))
- .forEach(accessibleTenant -> roleMemberships.add(Role.athenzTenantAdmin(accessibleTenant.name())));
- }));
-
- if ( identity.getDomain().equals(SCREWDRIVER_DOMAIN)
- && application.isPresent()
- && tenant.isPresent())
- futures.add(executor.submit(() -> {
- if ( tenant.get().type() == Tenant.Type.athenz
- && hasDeployerAccess(identity, ((AthenzTenant) tenant.get()).domain(), application.get(), zone))
- roleMemberships.add(Role.buildService(tenant.get().name(), application.get()));
- }));
-
- if (identity instanceof AthenzUser
- && zone.isPresent()
- && tenant.isPresent()
- && application.isPresent()) {
- ZoneId z = zone.get();
- futures.add(executor.submit(() -> {
- if (tenant.get().type() == Tenant.Type.athenz
- && canDeployToManualZones(identity, ((AthenzTenant) tenant.get()).domain(), application.get(), z))
- roleMemberships.add(Role.hostedDeveloper(tenant.get().name()));
- }));
- }
-
- futures.add(executor.submit(() -> {
- if (athenz.hasSystemFlagsAccess(identity, /*dryrun*/false))
- roleMemberships.add(Role.systemFlagsDeployer());
- }));
-
- futures.add(executor.submit(() -> {
- if (athenz.hasPaymentCallbackAccess(identity))
- roleMemberships.add(Role.paymentProcessor());
- }));
-
- futures.add(executor.submit(() -> {
- if (athenz.hasAccountingAccess(identity))
- roleMemberships.add(Role.hostedAccountant());
- }));
-
- // Run last request in handler thread to avoid creating extra thread.
- if (athenz.hasSystemFlagsAccess(identity, /*dryrun*/true))
- roleMemberships.add(Role.systemFlagsDryrunner());
-
- for (Future<?> future : futures)
- future.get(30, TimeUnit.SECONDS);
-
- logger.log(Level.FINE, () -> "Roles for principal (" + principal.getName() + "): " +
- roleMemberships.stream().map(role -> role.definition().name()).collect(Collectors.joining()));
-
- return roleMemberships.isEmpty()
- ? Set.of(Role.everyone())
- : Set.copyOf(roleMemberships);
- }
-
- @Override
- public void deconstruct() {
- try {
- executor.shutdown();
- if ( ! executor.awaitTermination(30, TimeUnit.SECONDS)) {
- executor.shutdownNow();
- if ( ! executor.awaitTermination(10, TimeUnit.SECONDS))
- throw new IllegalStateException("Failed to shut down executor 40 seconds");
- }
- }
- catch (InterruptedException e) {
- throw new IllegalStateException("Interrupted while shutting down executor", e);
- }
- }
-
- private boolean hasDeployerAccess(AthenzIdentity identity, AthenzDomain tenantDomain, ApplicationName application, Optional<ZoneId> zone) {
- try {
- return athenz.hasApplicationAccess(identity,
- ApplicationAction.deploy,
- tenantDomain,
- application,
- zone);
- } catch (ZmsClientException e) {
- throw new RuntimeException("Failed to authorize operation: (" + e.getMessage() + ")", e);
- }
- }
-
- private boolean canDeployToManualZones(AthenzIdentity identity, AthenzDomain tenantDomain, ApplicationName application, ZoneId zone) {
- if (! zone.environment().isManuallyDeployed()) return false;
- try {
- return athenz.hasApplicationAccess(identity, ApplicationAction.deploy, tenantDomain, application, Optional.of(zone));
- } catch (ZmsClientException e) {
- throw new RuntimeException("Failed to authorize operation: (" + e.getMessage() + ")", e);
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
deleted file mode 100644
index 115467ac805..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilter.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.filter;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.jdisc.Response;
-import com.yahoo.jdisc.http.HttpRequest;
-import com.yahoo.jdisc.http.filter.DiscFilterRequest;
-import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.role.Action;
-import com.yahoo.vespa.hosted.controller.api.role.Enforcer;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-
-import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * A security filter protects all controller apis.
- *
- * @author bjorncs
- */
-public class ControllerAuthorizationFilter extends JsonSecurityRequestFilterBase {
-
- private static final Logger log = Logger.getLogger(ControllerAuthorizationFilter.class.getName());
-
- private final Enforcer enforcer;
-
- @Inject
- public ControllerAuthorizationFilter(Controller controller) {
- this.enforcer = new Enforcer(controller.system());
- }
-
- @Override
- public Optional<ErrorResponse> filter(DiscFilterRequest request) {
- try {
- Optional<SecurityContext> securityContext = Optional.ofNullable((SecurityContext)request.getAttribute(SecurityContext.ATTRIBUTE_NAME));
-
- if (securityContext.isEmpty())
- return Optional.of(new ErrorResponse(Response.Status.UNAUTHORIZED, "Access denied - not authenticated"));
-
- Action action = Action.from(HttpRequest.Method.valueOf(request.getMethod()));
-
- // Avoid expensive look-ups when request is always legal.
- if (enforcer.allows(Role.everyone(), action, request.getUri()))
- return Optional.empty();
-
- Set<Role> roles = securityContext.get().roles();
- if (roles.stream().anyMatch(role -> enforcer.allows(role, action, request.getUri())))
- return Optional.empty();
- }
- catch (Exception e) {
- log.log(Level.WARNING, "Exception evaluating access control: ", e);
- }
- return Optional.of(new ErrorResponse(Response.Status.FORBIDDEN, "Access denied"));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java
deleted file mode 100644
index 114dfc8420c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilter.java
+++ /dev/null
@@ -1,73 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.filter;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.jdisc.http.filter.DiscFilterRequest;
-import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.TenantController;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.administrator;
-import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.developer;
-import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.user;
-
-/**
- * A security filter protects all controller apis.
- *
- * @author freva
- */
-public class LastLoginUpdateFilter extends JsonSecurityRequestFilterBase {
-
- private static final Logger log = Logger.getLogger(LastLoginUpdateFilter.class.getName());
-
- private final TenantController tenantController;
-
- @Inject
- public LastLoginUpdateFilter(Controller controller) {
- this.tenantController = controller.tenants();
- }
-
- @Override
- public Optional<ErrorResponse> filter(DiscFilterRequest request) {
- try {
- SecurityContext context = (SecurityContext) request.getAttribute(SecurityContext.ATTRIBUTE_NAME);
- Map<TenantName, List<LastLoginInfo.UserLevel>> userLevelsByTenant = context.roles().stream()
- .flatMap(LastLoginUpdateFilter::filterTenantUserLevels)
- .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
-
- userLevelsByTenant.forEach((tenant, userLevels) -> tenantController.updateLastLogin(tenant, userLevels, context.issuedAt()));
- } catch (Exception e) {
- log.log(Level.WARNING, "Exception updating last login:", e);
- }
- return Optional.empty();
- }
-
- public static Stream<Map.Entry<TenantName, LastLoginInfo.UserLevel>> filterTenantUserLevels(Role role) {
- if (!(role instanceof TenantRole))
- return Stream.empty();
-
- TenantRole tenantRole = (TenantRole) role;
- TenantName name = tenantRole.tenant();
- switch (tenantRole.definition()) {
- case athenzTenantAdmin:
- return Stream.of(Map.entry(name, user), Map.entry(name, developer), Map.entry(name, administrator));
- case reader: return Stream.of(Map.entry(name, user));
- case developer: return Stream.of(Map.entry(name, developer));
- case administrator: return Stream.of(Map.entry(name, administrator));
- default: return Stream.empty();
- }
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
deleted file mode 100644
index 7173b086b79..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilter.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.filter;
-
-import ai.vespa.hosted.api.Method;
-import ai.vespa.hosted.api.RequestVerifier;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.jdisc.http.filter.DiscFilterRequest;
-import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.yolean.Exceptions;
-
-import java.security.PublicKey;
-import java.util.Base64;
-import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * Assigns the {@link Role#headless(TenantName, ApplicationName)} role or
- * {@link Role#developer(TenantName)} to requests with a X-Authorization header signature
- * matching the public key of the indicated application.
- * Requests which already have a set of roles assigned to them are not modified.
- *
- * @author jonmv
- */
-public class SignatureFilter extends JsonSecurityRequestFilterBase {
-
- private static final Logger logger = Logger.getLogger(SignatureFilter.class.getName());
-
- private final Controller controller;
-
- @Inject
- public SignatureFilter(Controller controller) {
- this.controller = controller;
- }
-
- @Override
- protected Optional<ErrorResponse> filter(DiscFilterRequest request) {
- if ( request.getAttribute(SecurityContext.ATTRIBUTE_NAME) == null
- && request.getHeader("X-Authorization") != null)
- try {
- getSecurityContext(request).ifPresent(securityContext -> {
- request.setUserPrincipal(securityContext.principal());
- request.setRemoteUser(securityContext.principal().getName());
- request.setAttribute(SecurityContext.ATTRIBUTE_NAME, securityContext);
- });
- }
- catch (Exception e) {
- logger.log(Level.INFO, () -> "Exception verifying signed request: " + Exceptions.toMessageString(e));
- }
- return Optional.empty();
- }
-
- private boolean keyVerifies(PublicKey key, DiscFilterRequest request) {
- /* This method only checks that the content hash has been signed by the provided public key, but
- * does not verify the content of the request. jDisc request filters do not allow inspecting the
- * request body, so this responsibility falls on the handler consuming the body instead. For the
- * deployment cases, the request body is validated in {@link ApplicationApiHandler.parseDataParts}.
- */
- return new RequestVerifier(key, controller.clock()).verify(Method.valueOf(request.getMethod()),
- request.getUri(),
- request.getHeader("X-Timestamp"),
- request.getHeader("X-Content-Hash"),
- request.getHeader("X-Authorization"));
- }
-
- private Optional<SecurityContext> getSecurityContext(DiscFilterRequest request) {
- PublicKey key = KeyUtils.fromPemEncodedPublicKey(new String(Base64.getDecoder().decode(request.getHeader("X-Key")), UTF_8));
- if (keyVerifies(key, request)) {
- ApplicationId id = ApplicationId.fromSerializedForm(request.getHeader("X-Key-Id"));
- Optional<CloudTenant> tenant = controller.tenants().get(id.tenant())
- .filter(CloudTenant.class::isInstance)
- .map(CloudTenant.class::cast);
- if (tenant.isPresent() && tenant.get().developerKeys().containsKey(key))
- return Optional.of(new SecurityContext(tenant.get().developerKeys().get(key),
- Set.of(Role.reader(id.tenant()), Role.developer(id.tenant())),
- controller.clock().instant()));
-
- Optional <Application> application = controller.applications().getApplication(TenantAndApplicationId.from(id));
- if (application.isPresent() && application.get().deployKeys().contains(key))
- return Optional.of(new SecurityContext(new SimplePrincipal("headless@" + id.tenant() + "." + id.application()),
- Set.of(Role.reader(id.tenant()), Role.headless(id.tenant(), id.application())),
- controller.clock().instant()));
- }
- return Optional.empty();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.java
deleted file mode 100644
index 400576abfea..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsHandler.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.flags;
-
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.vespa.configserver.flags.FlagsDb;
-import com.yahoo.vespa.configserver.flags.http.FlagsHandler;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLogger;
-
-/**
- * An extension of {@link FlagsHandler} which logs requests to the audit log.
- *
- * @author mpolden
- */
-public class AuditedFlagsHandler extends FlagsHandler {
-
- private final AuditLogger auditLogger;
-
- public AuditedFlagsHandler(Context context, Controller controller, FlagsDb flagsDb) {
- super(context, flagsDb);
- auditLogger = controller.auditLogger();
- }
-
- @Override
- public HttpResponse handle(HttpRequest request) {
- return super.handle(auditLogger.log(request));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java
deleted file mode 100644
index 4f12f00eace..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiHandler.java
+++ /dev/null
@@ -1,169 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.horizon;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.vespa.flags.BooleanFlag;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.horizon.HorizonClient;
-import com.yahoo.vespa.hosted.controller.api.integration.horizon.HorizonResponse;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.yolean.Exceptions;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.EnumSet;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * Proxies metrics requests from Horizon UI
- *
- * @author valerijf
- */
-public class HorizonApiHandler extends ThreadedHttpRequestHandler {
-
- private final SystemName systemName;
- private final HorizonClient client;
- private final BooleanFlag enabledHorizonDashboard;
-
- private static final EnumSet<RoleDefinition> operatorRoleDefinitions =
- EnumSet.of(RoleDefinition.hostedOperator, RoleDefinition.hostedSupporter);
-
- @Inject
- public HorizonApiHandler(ThreadedHttpRequestHandler.Context parentCtx, Controller controller, FlagSource flagSource) {
- super(parentCtx);
- this.systemName = controller.system();
- this.client = controller.serviceRegistry().horizonClient();
- this.enabledHorizonDashboard = Flags.ENABLED_HORIZON_DASHBOARD.bindTo(flagSource);
- }
-
- @Override
- public HttpResponse handle(HttpRequest request) {
- var roles = getRoles(request);
- var operator = roles.stream().map(Role::definition).anyMatch(operatorRoleDefinitions::contains);
- var authorizedTenants = getAuthorizedTenants(roles);
-
- if (!operator && authorizedTenants.isEmpty())
- return ErrorResponse.forbidden("No tenant with enabled metrics view");
-
- try {
- return switch (request.getMethod()) {
- case GET -> get(request);
- case POST -> post(request, authorizedTenants, operator);
- case PUT -> put(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
- };
- }
- catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- }
- catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/horizon/v1/config/dashboard/topFolders")) return new JsonInputStreamResponse(client.getTopFolders());
- if (path.matches("/horizon/v1/config/dashboard/file/{id}")) return getDashboard(path.get("id"));
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse post(HttpRequest request, Set<TenantName> authorizedTenants, boolean operator) {
- Path path = new Path(request.getUri());
- if (path.matches("/horizon/v1/tsdb/api/query/graph")) return metricQuery(request, authorizedTenants, operator);
- if (path.matches("/horizon/v1/meta/search/timeseries")) return metaQuery(request, authorizedTenants, operator);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse put(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/horizon/v1/config/user")) return new JsonInputStreamResponse(client.getUser());
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse metricQuery(HttpRequest request, Set<TenantName> authorizedTenants, boolean operator) {
- try {
- byte[] data = TsdbQueryRewriter.rewrite(request.getData().readAllBytes(), authorizedTenants, operator, systemName);
- return new JsonInputStreamResponse(client.getMetrics(data));
- } catch (TsdbQueryRewriter.UnauthorizedException e) {
- return ErrorResponse.forbidden("Access denied");
- } catch (IOException e) {
- return ErrorResponse.badRequest("Failed to parse request body: " + e.getMessage());
- }
- }
-
- private HttpResponse metaQuery(HttpRequest request, Set<TenantName> authorizedTenants, boolean operator) {
- try {
- byte[] data = TsdbQueryRewriter.rewrite(request.getData().readAllBytes(), authorizedTenants, operator, systemName);
- return new JsonInputStreamResponse(client.getMetaData(data));
- } catch (TsdbQueryRewriter.UnauthorizedException e) {
- return ErrorResponse.forbidden("Access denied");
- } catch (IOException e) {
- return ErrorResponse.badRequest("Failed to parse request body: " + e.getMessage());
- }
- }
-
- private HttpResponse getDashboard(String id) {
- try {
- int dashboardId = Integer.parseInt(id);
- return new JsonInputStreamResponse(client.getDashboard(dashboardId));
- } catch (NumberFormatException e) {
- return ErrorResponse.badRequest("Dashboard ID must be integer, was " + id);
- }
- }
-
- private static Set<Role> getRoles(HttpRequest request) {
- return Optional.ofNullable(request.getJDiscRequest().context().get(SecurityContext.ATTRIBUTE_NAME))
- .filter(SecurityContext.class::isInstance)
- .map(SecurityContext.class::cast)
- .map(SecurityContext::roles)
- .orElseThrow(() -> new IllegalArgumentException("Attribute '" + SecurityContext.ATTRIBUTE_NAME + "' was not set on request"));
- }
-
- private Set<TenantName> getAuthorizedTenants(Set<Role> roles) {
- return roles.stream()
- .filter(TenantRole.class::isInstance)
- .map(role -> ((TenantRole) role).tenant())
- .filter(tenant -> enabledHorizonDashboard.with(FetchVector.Dimension.TENANT_ID, tenant.value()).value())
- .collect(Collectors.toSet());
- }
-
- private static class JsonInputStreamResponse extends HttpResponse {
-
- private final HorizonResponse response;
-
- public JsonInputStreamResponse(HorizonResponse response) {
- super(response.code());
- this.response = response;
- }
-
- @Override
- public String getContentType() {
- return "application/json";
- }
-
- @Override
- public void render(OutputStream outputStream) throws IOException {
- try (InputStream inputStream = response.inputStream()) {
- inputStream.transferTo(outputStream);
- }
- }
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java
deleted file mode 100644
index 2f3957af70d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriter.java
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.horizon;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-
-import java.io.IOException;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-/**
- * @author valerijf
- */
-public class TsdbQueryRewriter {
-
- private static final ObjectMapper mapper = new ObjectMapper();
-
- public static byte[] rewrite(byte[] data, Set<TenantName> authorizedTenants, boolean operator, SystemName systemName) throws IOException {
- JsonNode root = mapper.readTree(data);
- requireLegalType(root);
- getField(root, "executionGraph", ArrayNode.class)
- .ifPresent(graph -> rewriteQueryGraph(root, graph, authorizedTenants, operator, systemName));
- getField(root, "filters", ArrayNode.class)
- .ifPresent(filters -> rewriteFilters(filters, authorizedTenants, operator, systemName));
- getField(root, "queries", ArrayNode.class)
- .ifPresent(graph -> rewriteQueryGraph(root, graph, authorizedTenants, operator, systemName));
-
- return mapper.writeValueAsBytes(root);
- }
-
- private static void rewriteQueryGraph(JsonNode root, ArrayNode executionGraph, Set<TenantName> tenantNames, boolean operator, SystemName systemName) {
- for (int i = 0; i < executionGraph.size(); i++) {
- JsonNode execution = executionGraph.get(i);
-
- // Will be handled by rewriteFilters()
- if (execution.has("filterId")) {
- if (filterExists(root, execution.get("filterId").asText()))
- continue;
- else
- throw new IllegalArgumentException("Invalid filterId: " + execution.get("filterId").asText());
- }
-
- rewriteFilter((ObjectNode) execution, tenantNames, operator, systemName);
- }
- }
-
- private static void rewriteFilters(ArrayNode filters, Set<TenantName> tenantNames, boolean operator, SystemName systemName) {
- for (int i = 0; i < filters.size(); i++)
- rewriteFilter((ObjectNode) filters.get(i), tenantNames, operator, systemName);
- }
-
- private static void rewriteFilter(ObjectNode parent, Set<TenantName> tenantNames, boolean operator, SystemName systemName) {
- ObjectNode prev = ((ObjectNode) parent.get("filter"));
- ArrayNode filters;
- // If we dont already have a filter object, or the object that we have is not an AND filter
- if (prev == null || !"Chain".equals(prev.get("type").asText()) || prev.get("op") != null && !"AND".equals(prev.get("op").asText())) {
- // Create new filter object
- filters = parent.putObject("filter")
- .put("type", "Chain")
- .put("op", "AND")
- .putArray("filters");
-
- // Add the previous filter to the AND expression
- if (prev != null) filters.add(prev);
- } else filters = (ArrayNode) prev.get("filters");
-
- // Make sure we only show metrics in the relevant system
- ObjectNode systemFilter = filters.addObject();
- systemFilter.put("type", "TagValueLiteralOr");
- systemFilter.put("filter", systemName.name().toLowerCase());
- systemFilter.put("tagKey", "system");
-
- // Make sure non-operators cannot see metrics outside of their tenants
- if (!operator) {
- ObjectNode appFilter = filters.addObject();
- appFilter.put("type", "TagValueRegex");
- appFilter.put("filter",
- tenantNames.stream().map(TenantName::value).sorted().collect(Collectors.joining("|", "^(", ")\\..*")));
- appFilter.put("tagKey", "applicationId");
- }
- }
-
- private static boolean filterExists(JsonNode root, String filterId) {
- return getField(root, "filters", ArrayNode.class).stream()
- .flatMap(filters -> IntStream.range(0, filters.size())
- .mapToObj(i -> filters.get(i).get("id")))
- .filter(Objects::nonNull)
- .filter(JsonNode::isTextual)
- .map(JsonNode::asText)
- .anyMatch(filterId::equals);
- }
-
- private static void requireLegalType(JsonNode root) {
- Optional.ofNullable(root.get("type"))
- .map(JsonNode::asText)
- .filter(type -> !"TAG_KEYS_AND_VALUES".equals(type))
- .ifPresent(type -> { throw new IllegalArgumentException("Illegal type " + type); });
- }
-
- private static <T extends JsonNode> Optional<T> getField(JsonNode object, String fieldName, Class<T> clazz) {
- return Optional.ofNullable(object.get(fieldName)).filter(clazz::isInstance).map(clazz::cast);
- }
-
- static class UnauthorizedException extends RuntimeException { }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java
deleted file mode 100644
index 701761895c3..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiHandler.java
+++ /dev/null
@@ -1,265 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.os;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.config.provision.zone.ZoneList;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.io.IOUtils;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.MessageResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.slime.Type;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
-import com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance;
-import com.yahoo.vespa.hosted.controller.maintenance.OsUpgradeScheduler;
-import com.yahoo.vespa.hosted.controller.maintenance.OsUpgradeScheduler.Change;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.vespa.hosted.controller.versions.CertifiedOsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
-import com.yahoo.yolean.Exceptions;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-import java.util.Scanner;
-import java.util.Set;
-import java.util.StringJoiner;
-import java.util.function.Function;
-
-/**
- * This implements the /os/v1 API which provides operators with information about, and scheduling of OS upgrades for
- * nodes in the system.
- *
- * @author mpolden
- */
-@SuppressWarnings("unused") // Injected
-public class OsApiHandler extends AuditLoggingRequestHandler {
-
- private final Controller controller;
- private final OsUpgradeScheduler osUpgradeScheduler;
-
- public OsApiHandler(Context ctx, Controller controller, ControllerMaintenance controllerMaintenance) {
- super(ctx, controller.auditLogger());
- this.controller = controller;
- this.osUpgradeScheduler = controllerMaintenance.osUpgradeScheduler();
- }
-
- @Override
- public HttpResponse auditAndHandle(HttpRequest request) {
- try {
- return switch (request.getMethod()) {
- case GET -> get(request);
- case POST -> post(request);
- case DELETE -> delete(request);
- case PATCH -> patch(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
- };
- } catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- } catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse patch(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/os/v1/")) return setOsVersion(request);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/os/v1/")) return new SlimeJsonResponse(osVersions());
- if (path.matches("/os/v1/certify")) return new SlimeJsonResponse(certifiedOsVersions());
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse post(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/os/v1/certify/{cloud}/{version}")) return certifyVersion(request, path.get("version"), path.get("cloud"));
- if (path.matches("/os/v1/firmware/")) return requestFirmwareCheckResponse(path);
- if (path.matches("/os/v1/firmware/{environment}/")) return requestFirmwareCheckResponse(path);
- if (path.matches("/os/v1/firmware/{environment}/{region}/")) return requestFirmwareCheckResponse(path);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse delete(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/os/v1/certify/{cloud}/{version}")) return uncertifyVersion(request, path.get("version"), path.get("cloud"));
- if (path.matches("/os/v1/firmware/")) return cancelFirmwareCheckResponse(path);
- if (path.matches("/os/v1/firmware/{environment}/")) return cancelFirmwareCheckResponse(path);
- if (path.matches("/os/v1/firmware/{environment}/{region}/")) return cancelFirmwareCheckResponse(path);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse certifyVersion(HttpRequest request, String versionString, String cloudName) {
- Version version = Version.fromString(versionString);
- CloudName cloud = CloudName.from(cloudName);
- String vespaVersionString = asString(request.getData());
- if (vespaVersionString.isEmpty()) {
- throw new IllegalArgumentException("Missing Vespa version in request body");
- }
- Version vespaVersion = Version.fromString(vespaVersionString);
- CertifiedOsVersion certified = controller.os().certify(version, cloud, vespaVersion);
- if (certified.vespaVersion().equals(vespaVersion)) {
- return new MessageResponse("Certified " + version.toFullString() + " in cloud " + cloud +
- " as compatible with Vespa version " + vespaVersion.toFullString());
- }
- return new MessageResponse(version.toFullString() + " is already certified in cloud " + cloud +
- " as compatible with Vespa version " + certified.vespaVersion().toFullString() +
- ". Leaving certification unchanged");
- }
-
- private HttpResponse uncertifyVersion(HttpRequest request, String versionString, String cloudName) {
- Version version = Version.fromString(versionString);
- CloudName cloud = CloudName.from(cloudName);
- controller.os().uncertify(version, cloud);
- return new MessageResponse("Removed certification of " + version.toFullString() + " in cloud " + cloud);
- }
-
- private HttpResponse requestFirmwareCheckResponse(Path path) {
- List<ZoneId> zones = zonesAt(path);
- if (zones.isEmpty())
- return ErrorResponse.notFoundError("No zones at " + path);
-
- StringJoiner response = new StringJoiner(", ", "Requested firmware checks in ", ".");
- for (ZoneId zone : zones) {
- controller.serviceRegistry().configServer().nodeRepository().requestFirmwareCheck(zone);
- response.add(zone.value());
- }
- return new MessageResponse(response.toString());
- }
-
- private HttpResponse cancelFirmwareCheckResponse(Path path) {
- List<ZoneId> zones = zonesAt(path);
- if (zones.isEmpty())
- return ErrorResponse.notFoundError("No zones at " + path);
-
- StringJoiner response = new StringJoiner(", ", "Cancelled firmware checks in ", ".");
- for (ZoneId zone : zones) {
- controller.serviceRegistry().configServer().nodeRepository().cancelFirmwareCheck(zone);
- response.add(zone.value());
- }
- return new MessageResponse(response.toString());
- }
-
- private List<ZoneId> zonesAt(Path path) {
- ZoneList zones = controller.zoneRegistry().zones().controllerUpgraded();
- if (path.get("region") != null) zones = zones.in(RegionName.from(path.get("region")));
- if (path.get("environment") != null) zones = zones.in(Environment.from(path.get("environment")));
- return zones.zones().stream().map(ZoneApi::getId).toList();
- }
-
- private HttpResponse setOsVersion(HttpRequest request) {
- Slime requestData = toSlime(request.getData());
- Inspector root = requestData.get();
- CloudName cloud = parseStringField("cloud", root, CloudName::from);
- if (requireField("version", root).type() == Type.NIX) {
- controller.os().cancelUpgrade(cloud);
- return new MessageResponse("Cleared target OS version for cloud '" + cloud.value() + "'");
- }
- Version target = parseStringField("version", root, Version::fromString);
- boolean force = root.field("force").asBool();
- boolean pin = root.field("pin").asBool();
- controller.os().upgradeTo(target, cloud, force, pin);
- return new MessageResponse("Set target OS version for cloud '" + cloud.value() + "' to " +
- target.toFullString() + (pin ? " (pinned)" : ""));
- }
-
- private Slime certifiedOsVersions() {
- Slime slime = new Slime();
- Cursor array = slime.setArray();
- controller.os().readCertified().stream().sorted().forEach(cv -> {
- Cursor object = array.addObject();
- object.setString("version", cv.osVersion().version().toFullString());
- object.setString("cloud", cv.osVersion().cloud().value());
- object.setString("vespaVersion", cv.vespaVersion().toFullString());
- });
- return slime;
- }
-
- private Slime osVersions() {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Set<OsVersionTarget> targets = controller.os().targets();
-
- Cursor versions = root.setArray("versions");
- Instant now = controller.clock().instant();
- controller.os().status().versions().forEach((osVersion, nodeVersions) -> {
- Cursor currentVersionObject = versions.addObject();
- currentVersionObject.setString("version", osVersion.version().toFullString());
- Optional<OsVersionTarget> target = targets.stream().filter(t -> t.osVersion().equals(osVersion)).findFirst();
- currentVersionObject.setBool("targetVersion", target.isPresent());
- target.ifPresent(t -> {
- currentVersionObject.setString("upgradeBudget", Duration.ZERO.toString());
- currentVersionObject.setLong("scheduledAt", t.scheduledAt().toEpochMilli());
- currentVersionObject.setBool("pinned", t.pinned());
- Optional<Change> nextChange = osUpgradeScheduler.changeIn(t.osVersion().cloud(), now, true);
- nextChange.ifPresent(c -> {
- currentVersionObject.setString("nextVersion", c.osVersion().version().toFullString());
- currentVersionObject.setLong("nextScheduledAt", c.scheduleAt().toEpochMilli());
- currentVersionObject.setBool("certified", c.certified());
- });
- });
-
- currentVersionObject.setString("cloud", osVersion.cloud().value());
- Cursor nodesArray = currentVersionObject.setArray("nodes");
- nodeVersions.forEach(nodeVersion -> {
- Cursor nodeObject = nodesArray.addObject();
- nodeObject.setString("hostname", nodeVersion.hostname().value());
- nodeObject.setString("environment", nodeVersion.zone().environment().value());
- nodeObject.setString("region", nodeVersion.zone().region().value());
- });
- });
-
- return slime;
- }
-
- private static Slime toSlime(InputStream json) {
- try {
- return SlimeUtils.jsonToSlime(IOUtils.readBytes(json, 1000 * 1000));
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- private static <T> T parseStringField(String name, Inspector root, Function<String, T> parser) {
- String fieldValue = requireField(name, root).asString();
- try {
- return parser.apply(fieldValue);
- } catch (Exception e) {
- throw new IllegalArgumentException("Invalid " + name + " '" + fieldValue + "'", e);
- }
- }
-
- private static Inspector requireField(String name, Inspector root) {
- Inspector field = root.field(name);
- if (!field.valid()) throw new IllegalArgumentException("Field '" + name + "' is required");
- return field;
- }
-
- private static String asString(InputStream in) {
- Scanner scanner = new Scanner(in).useDelimiter("\\A");
- if (scanner.hasNext()) {
- return scanner.next();
- }
- return "";
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java
deleted file mode 100644
index 2a6778870b1..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiHandler.java
+++ /dev/null
@@ -1,374 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.routing;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.MessageResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.ResourceResponse;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.EndpointList;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext;
-import com.yahoo.vespa.hosted.controller.routing.context.RoutingContext;
-import com.yahoo.yolean.Exceptions;
-
-import java.net.URI;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-/**
- * This implements the /routing/v1 API, which provides operators and tenants routing control at both zone- (operator
- * only) and deployment-level.
- *
- * @author mpolden
- */
-public class RoutingApiHandler extends AuditLoggingRequestHandler {
-
- private final Controller controller;
-
- public RoutingApiHandler(Context ctx, Controller controller) {
- super(ctx, controller.auditLogger());
- this.controller = Objects.requireNonNull(controller, "controller must be non-null");
- }
-
- @Override
- public HttpResponse auditAndHandle(HttpRequest request) {
- try {
- var path = new Path(request.getUri());
- return switch (request.getMethod()) {
- case GET -> get(path, request);
- case POST -> post(path, request);
- case DELETE -> delete(path, request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
- };
- } catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- } catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse delete(Path path, HttpRequest request) {
- if (path.matches("/routing/v1/inactive/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return setDeploymentStatus(path, true, request);
- if (path.matches("/routing/v1/inactive/environment/{environment}/region/{region}")) return setZoneStatus(path, true);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse post(Path path, HttpRequest request) {
- if (path.matches("/routing/v1/inactive/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return setDeploymentStatus(path, false, request);
- if (path.matches("/routing/v1/inactive/environment/{environment}/region/{region}")) return setZoneStatus(path, false);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse get(Path path, HttpRequest request) {
- if (path.matches("/routing/v1/")) return status(request.getUri());
- if (path.matches("/routing/v1/status/tenant/{tenant}")) return tenant(path, request);
- if (path.matches("/routing/v1/status/tenant/{tenant}/application/{application}")) return application(path, request);
- if (path.matches("/routing/v1/status/tenant/{tenant}/application/{application}/instance/{instance}")) return instance(path, request);
- if (path.matches("/routing/v1/status/tenant/{tenant}/application/{application}/instance/{instance}/environment/{environment}/region/{region}")) return deployment(path);
- if (path.matches("/routing/v1/status/tenant/{tenant}/application/{application}/instance/{instance}/endpoint")) return endpoints(path);
- if (path.matches("/routing/v1/status/environment")) return environment(request);
- if (path.matches("/routing/v1/status/environment/{environment}/region/{region}")) return zone(path);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse endpoints(Path path) {
- ApplicationId instanceId = instanceFrom(path);
- List<Endpoint> endpoints = controller.routing().readDeclaredEndpointsOf(instanceId)
- .sortedBy(Comparator.comparing(Endpoint::dnsName))
- .asList();
-
- List<DeploymentId> deployments = endpoints.stream()
- .flatMap(e -> e.deployments().stream())
- .distinct()
- .toList();
-
- Map<DeploymentId, RoutingStatus> deploymentsStatus = deployments.stream()
- .collect(Collectors.toMap(
- deploymentId -> deploymentId,
- deploymentId -> controller.routing().of(deploymentId).routingStatus())
- );
-
- var slime = new Slime();
- var root = slime.setObject();
- var endpointsRoot = root.setArray("endpoints");
- endpoints.forEach(endpoint -> {
- var endpointRoot = endpointsRoot.addObject();
- endpointToSlime(endpointRoot, endpoint);
- var zonesRoot = endpointRoot.setArray("zones");
- endpoint.deployments().stream().sorted(Comparator.comparing(d -> d.zoneId().value()))
- .forEach(deployment -> {
- RoutingStatus status = deploymentsStatus.get(deployment);
- deploymentStatusToSlime(zonesRoot.addObject(), deployment, status, endpoint.routingMethod());
- });
- });
-
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse environment(HttpRequest request) {
- var zones = controller.zoneRegistry().zones().all().ids();
- if (isRecursive(request)) {
- var slime = new Slime();
- var root = slime.setObject();
- var zonesArray = root.setArray("zones");
- for (var zone : zones) {
- toSlime(zone, zonesArray.addObject());
- }
- return new SlimeJsonResponse(slime);
- }
- var resources = controller.zoneRegistry().zones().all().ids().stream()
- .map(zone -> zone.environment().value() +
- "/region/" + zone.region().value())
- .sorted()
- .toList();
- return new ResourceResponse(request.getUri(), resources);
- }
-
- private HttpResponse status(URI requestUrl) {
- return new ResourceResponse(requestUrl, "status/tenant", "status/environment");
- }
-
- private HttpResponse tenant(Path path, HttpRequest request) {
- var tenantName = tenantFrom(path);
- if (isRecursive(request)) {
- var slime = new Slime();
- var root = slime.setObject();
- toSlime(controller.applications().asList(tenantName), null, null, root);
- return new SlimeJsonResponse(slime);
- }
- var resources = controller.applications().asList(tenantName).stream()
- .map(Application::id)
- .map(TenantAndApplicationId::application)
- .map(ApplicationName::value)
- .map(application -> "application/" + application)
- .sorted()
- .toList();
- return new ResourceResponse(request.getUri(), resources);
- }
-
- private HttpResponse application(Path path, HttpRequest request) {
- var tenantAndApplicationId = tenantAndApplicationIdFrom(path);
- if (isRecursive(request)) {
- var slime = new Slime();
- var root = slime.setObject();
- toSlime(List.of(controller.applications().requireApplication(tenantAndApplicationId)), null,
- null, root);
- return new SlimeJsonResponse(slime);
- }
- var resources = controller.applications().requireApplication(tenantAndApplicationId).instances().keySet().stream()
- .map(InstanceName::value)
- .map(instance -> "instance/" + instance)
- .sorted()
- .toList();
- return new ResourceResponse(request.getUri(), resources);
- }
-
- private HttpResponse instance(Path path, HttpRequest request) {
- var instanceId = instanceFrom(path);
- if (isRecursive(request)) {
- var slime = new Slime();
- var root = slime.setObject();
- toSlime(List.of(controller.applications().requireApplication(TenantAndApplicationId.from(instanceId))),
- instanceId, null, root);
- return new SlimeJsonResponse(slime);
- }
- var resources = controller.applications().requireInstance(instanceId).deployments().keySet().stream()
- .map(zone -> "environment/" + zone.environment().value() +
- "/region/" + zone.region().value())
- .sorted()
- .toList();
- return new ResourceResponse(request.getUri(), resources);
- }
-
- private HttpResponse setZoneStatus(Path path, boolean in) {
- ZoneId zone = zoneFrom(path);
- RoutingContext context = controller.routing().of(zone);
- RoutingStatus.Value newStatus = in ? RoutingStatus.Value.in : RoutingStatus.Value.out;
- context.setRoutingStatus(newStatus, RoutingStatus.Agent.operator);
- return new MessageResponse("Set global routing status for deployments in " + zone + " to " +
- (in ? "IN" : "OUT"));
- }
-
- private HttpResponse zone(Path path) {
- var zone = zoneFrom(path);
- var slime = new Slime();
- var root = slime.setObject();
- toSlime(zone, root);
- return new SlimeJsonResponse(slime);
- }
-
- private void toSlime(ZoneId zone, Cursor zoneObject) {
- RoutingContext context = controller.routing().of(zone);
- zoneStatusToSlime(zoneObject, zone, context.routingStatus(), context.routingMethod());
- }
-
- private HttpResponse setDeploymentStatus(Path path, boolean in, HttpRequest request) {
- var deployment = deploymentFrom(path);
- var instance = controller.applications().requireInstance(deployment.applicationId());
- var status = in ? RoutingStatus.Value.in : RoutingStatus.Value.out;
- var agent = isOperator(request) ? RoutingStatus.Agent.operator : RoutingStatus.Agent.tenant;
- requireDeployment(deployment, instance);
- controller.routing().of(deployment).setRoutingStatus(status, agent);
- return new MessageResponse("Set global routing status for " + deployment + " to " + (in ? "IN" : "OUT"));
- }
-
- private HttpResponse deployment(Path path) {
- var slime = new Slime();
- var root = slime.setObject();
- var deploymentId = deploymentFrom(path);
- var application = controller.applications().requireApplication(TenantAndApplicationId.from(deploymentId.applicationId()));
- toSlime(List.of(application), deploymentId.applicationId(), deploymentId.zoneId(), root);
- return new SlimeJsonResponse(slime);
- }
-
- private void toSlime(List<Application> applications, ApplicationId instanceId, ZoneId zoneId, Cursor root) {
- var deploymentsArray = root.setArray("deployments");
- for (var application : applications) {
- var instances = instanceId == null
- ? application.instances().values()
- : List.of(application.require(instanceId.instance()));
- EndpointList declaredEndpoints = controller.routing().readDeclaredEndpointsOf(application);
- for (var instance : instances) {
- var zones = zoneId == null
- ? instance.deployments().keySet().stream().sorted(Comparator.comparing(ZoneId::value)).toList()
- : List.of(zoneId);
- for (var zone : zones) {
- DeploymentId deploymentId = requireDeployment(new DeploymentId(instance.id(), zone), instance);
- DeploymentRoutingContext context = controller.routing().of(deploymentId);
- if (declaredEndpoints.targets(deploymentId).isEmpty()) continue; // No declared endpoints point to this deployment
- deploymentStatusToSlime(deploymentsArray.addObject(),
- deploymentId,
- context.routingStatus(),
- context.routingMethod());
- }
- }
- }
-
- }
-
- private static void zoneStatusToSlime(Cursor object, ZoneId zone, RoutingStatus routingStatus, RoutingMethod method) {
- object.setString("routingMethod", asString(method));
- object.setString("environment", zone.environment().value());
- object.setString("region", zone.region().value());
- object.setString("status", asString(routingStatus.value()));
- object.setString("agent", asString(routingStatus.agent()));
- object.setLong("changedAt", routingStatus.changedAt().toEpochMilli());
- }
-
- private static void deploymentStatusToSlime(Cursor object, DeploymentId deployment, RoutingStatus routingStatus, RoutingMethod method) {
- object.setString("routingMethod", asString(method));
- object.setString("instance", deployment.applicationId().serializedForm());
- object.setString("environment", deployment.zoneId().environment().value());
- object.setString("region", deployment.zoneId().region().value());
- object.setString("status", asString(routingStatus.value()));
- object.setString("agent", asString(routingStatus.agent()));
- object.setLong("changedAt", routingStatus.changedAt().toEpochMilli());
- }
-
- private static void endpointToSlime(Cursor object, Endpoint endpoint) {
- object.setString("name", endpoint.name());
- object.setString("dnsName", endpoint.dnsName());
- object.setString("routingMethod", endpoint.routingMethod().name());
- object.setString("cluster", endpoint.cluster().value());
- object.setString("scope", endpoint.scope().name());
- }
-
- private TenantName tenantFrom(Path path) {
- return TenantName.from(path.get("tenant"));
- }
-
- private ApplicationName applicationFrom(Path path) {
- return ApplicationName.from(path.get("application"));
- }
-
- private TenantAndApplicationId tenantAndApplicationIdFrom(Path path) {
- return TenantAndApplicationId.from(tenantFrom(path), applicationFrom(path));
- }
-
- private ApplicationId instanceFrom(Path path) {
- return ApplicationId.from(tenantFrom(path), applicationFrom(path), InstanceName.from(path.get("instance")));
- }
-
- private DeploymentId deploymentFrom(Path path) {
- return new DeploymentId(instanceFrom(path), zoneFrom(path));
- }
-
- private ZoneId zoneFrom(Path path) {
- var zone = ZoneId.from(path.get("environment"), path.get("region"));
- if (!controller.zoneRegistry().hasZone(zone)) {
- throw new IllegalArgumentException("No such zone: " + zone);
- }
- return zone;
- }
-
- private static DeploymentId requireDeployment(DeploymentId deployment, Instance instance) {
- if (!instance.deployments().containsKey(deployment.zoneId())) {
- throw new IllegalArgumentException("No such deployment: " + deployment);
- }
- return deployment;
- }
-
- private static boolean isOperator(HttpRequest request) {
- SecurityContext securityContext = Optional.ofNullable(request.getJDiscRequest().context().get(SecurityContext.ATTRIBUTE_NAME))
- .filter(SecurityContext.class::isInstance)
- .map(SecurityContext.class::cast)
- .orElseThrow(() -> new IllegalArgumentException("Attribute '" + SecurityContext.ATTRIBUTE_NAME + "' was not set on request"));
- return securityContext.roles().stream()
- .map(Role::definition)
- .anyMatch(definition -> definition == RoleDefinition.hostedOperator);
- }
-
- private static boolean isRecursive(HttpRequest request) {
- return "true".equals(request.getProperty("recursive"));
- }
-
- private static String asString(RoutingStatus.Value value) {
- return switch (value) {
- case in -> "in";
- case out -> "out";
- };
- }
-
- private static String asString(RoutingStatus.Agent agent) {
- return switch (agent) {
- case operator -> "operator";
- case system -> "system";
- case tenant -> "tenant";
- case unknown -> "unknown";
- };
- }
-
- private static String asString(RoutingMethod method) {
- return switch (method) {
- case exclusive -> "exclusive";
- case sharedLayer4 -> "sharedLayer4";
- };
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java
deleted file mode 100644
index 2b53b1a32f5..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClient.java
+++ /dev/null
@@ -1,202 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.systemflags;
-
-import ai.vespa.util.http.hc4.SslConnectionSocketFactory;
-import ai.vespa.util.http.hc4.retry.DelayedConnectionLevelRetryHandler;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.tls.AthenzIdentityVerifier;
-import com.yahoo.vespa.flags.FlagId;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.hosted.controller.api.integration.ControllerIdentityProvider;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire.WireErrorResponse;
-import org.apache.http.HttpEntity;
-import org.apache.http.HttpResponse;
-import org.apache.http.HttpStatus;
-import org.apache.http.NameValuePair;
-import org.apache.http.client.ResponseHandler;
-import org.apache.http.client.config.RequestConfig;
-import org.apache.http.client.methods.HttpDelete;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPut;
-import org.apache.http.client.methods.HttpUriRequest;
-import org.apache.http.client.utils.URIBuilder;
-import org.apache.http.entity.ContentType;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClientBuilder;
-import org.apache.http.message.BasicNameValuePair;
-import org.apache.http.util.EntityUtils;
-
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLSession;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
-import static java.util.stream.Collectors.toSet;
-
-/**
- * A client for /flags/v1 rest api on configserver and controller.
- *
- * @author bjorncs
- */
-class FlagsClient {
-
- private static final String FLAGS_V1_PATH = "/flags/v1";
-
- private static final ObjectMapper mapper = new ObjectMapper();
-
- private final CloseableHttpClient client;
-
- FlagsClient(ControllerIdentityProvider identityProvider, Set<FlagsTarget> targets) {
- this.client = createClient(identityProvider, targets);
- }
-
- List<FlagData> listFlagData(FlagsTarget target) throws FlagsException, UncheckedIOException {
- HttpGet request = new HttpGet(createUri(target, "/data", List.of(new BasicNameValuePair("recursive", "true"))));
- return executeRequest(request, response -> {
- verifySuccess(response, null);
- return FlagData.deserializeList(EntityUtils.toByteArray(response.getEntity()));
- });
- }
-
- List<FlagId> listDefinedFlags(FlagsTarget target) {
- HttpGet request = new HttpGet(createUri(target, "/defined", List.of()));
- return executeRequest(request, response -> {
- verifySuccess(response, null);
- JsonNode json = mapper.readTree(response.getEntity().getContent());
- List<FlagId> flagIds = new ArrayList<>();
- json.fieldNames().forEachRemaining(fieldName -> flagIds.add(new FlagId(fieldName)));
- return flagIds;
- });
- }
-
- void putFlagData(FlagsTarget target, FlagData flagData) throws FlagsException, UncheckedIOException {
- HttpPut request = new HttpPut(createUri(target, "/data/" + flagData.id().toString(), List.of()));
- request.setEntity(jsonContent(flagData.serializeToJson()));
- executeRequest(request, response -> {
- verifySuccess(response, flagData.id());
- return null;
- });
- }
-
- void deleteFlagData(FlagsTarget target, FlagId flagId) throws FlagsException, UncheckedIOException {
- HttpDelete request = new HttpDelete(createUri(target, "/data/" + flagId.toString(), List.of(new BasicNameValuePair("force", "true"))));
- executeRequest(request, response -> {
- verifySuccess(response, flagId);
- return null;
- });
- }
-
- private static CloseableHttpClient createClient(ControllerIdentityProvider identityProvider, Set<FlagsTarget> targets) {
- DelayedConnectionLevelRetryHandler retryHandler = DelayedConnectionLevelRetryHandler.Builder
- .withExponentialBackoff(Duration.ofSeconds(1), Duration.ofSeconds(20), 5)
- .build();
-
- return HttpClientBuilder.create()
- .setUserAgent("controller-flags-v1-client")
- .setSSLSocketFactory(SslConnectionSocketFactory.of(
- identityProvider.getConfigServerSslSocketFactory(), new FlagTargetsHostnameVerifier(targets)))
- .setDefaultRequestConfig(RequestConfig.custom()
- .setConnectTimeout((int) Duration.ofSeconds(10).toMillis())
- .setConnectionRequestTimeout((int) Duration.ofSeconds(10).toMillis())
- .setSocketTimeout((int) Duration.ofSeconds(20).toMillis())
- .build())
- .setMaxConnPerRoute(2)
- .setMaxConnTotal(100)
- .setRetryHandler(retryHandler)
- .build();
- }
-
- private <T> T executeRequest(HttpUriRequest request, ResponseHandler<T> handler) {
- try {
- return client.execute(request, handler);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- private static URI createUri(FlagsTarget target, String subPath, List<NameValuePair> queryParams) {
- try {
- return new URIBuilder(target.endpoint())
- .setPath(FLAGS_V1_PATH + subPath)
- .setParameters(queryParams)
- .build();
- } catch (URISyntaxException e) {
- throw new RuntimeException(e); // should never happen
- }
- }
-
- private static void verifySuccess(HttpResponse response, FlagId flagId) throws IOException {
- if (!success(response)) {
- throw createFlagsException(response, flagId);
- }
- }
-
- private static FlagsException createFlagsException(HttpResponse response, FlagId flagId) throws IOException {
- HttpEntity entity = response.getEntity();
- String content = EntityUtils.toString(entity);
- int statusCode = response.getStatusLine().getStatusCode();
- if (ContentType.get(entity).getMimeType().equals(ContentType.APPLICATION_JSON.getMimeType())) {
- WireErrorResponse error = mapper.readValue(content, WireErrorResponse.class);
- return new FlagsException(statusCode, flagId, error.errorCode, error.message);
- } else {
- return new FlagsException(statusCode, flagId, null, content);
- }
- }
-
- private static boolean success(HttpResponse response) {
- return response.getStatusLine().getStatusCode() == HttpStatus.SC_OK;
- }
-
- private static StringEntity jsonContent(String json) {
- return new StringEntity(json, ContentType.APPLICATION_JSON);
- }
-
- private static class FlagTargetsHostnameVerifier implements HostnameVerifier {
-
- private final AthenzIdentityVerifier athenzVerifier;
-
- FlagTargetsHostnameVerifier(Set<FlagsTarget> targets) {
- this.athenzVerifier = createAthenzIdentityVerifier(targets);
- }
-
- private static AthenzIdentityVerifier createAthenzIdentityVerifier(Set<FlagsTarget> targets) {
- Set<AthenzIdentity> identities = targets.stream()
- .flatMap(target -> target.athenzHttpsIdentity().stream())
- .collect(toSet());
- return new AthenzIdentityVerifier(identities);
- }
-
- @Override
- public boolean verify(String hostname, SSLSession session) {
- return "localhost".equals(hostname) /* for controllers */ || athenzVerifier.verify(hostname, session);
- }
- }
-
- static class FlagsException extends RuntimeException {
-
- private FlagsException(int statusCode, FlagId flagId, String errorCode, String errorMessage) {
- super(createErrorMessage(statusCode, flagId, errorCode, errorMessage));
- }
-
- private static String createErrorMessage(int statusCode, FlagId flagId, String errorCode, String errorMessage) {
- StringBuilder builder = new StringBuilder().append("Received ").append(statusCode);
- if (errorCode != null) {
- builder.append('/').append(errorCode);
- }
- if (flagId != null) {
- builder.append(" for flag '").append(flagId).append("'");
- }
- return builder.append(": ").append(errorMessage).toString();
- }
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClientException.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClientException.java
deleted file mode 100644
index e1b3da65e6e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/FlagsClientException.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.systemflags;
-
-import java.util.OptionalInt;
-
-/**
- * @author bjorncs
- */
-class FlagsClientException extends RuntimeException {
-
- private final int responseCode;
-
- FlagsClientException(int responseCode, String message) {
- super(message);
- this.responseCode = responseCode;
- }
-
- FlagsClientException(String message, Throwable cause) {
- super(message, cause);
- this.responseCode = -1;
- }
-
- OptionalInt responseCode() {
- return responseCode > 0 ? OptionalInt.of(responseCode) : OptionalInt.empty();
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java
deleted file mode 100644
index c006fa13223..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResult.java
+++ /dev/null
@@ -1,431 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.systemflags;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.yahoo.vespa.flags.FlagDefinition;
-import com.yahoo.vespa.flags.FlagId;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire.WireSystemFlagsDeployResult;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire.WireSystemFlagsDeployResult.WireFlagDataChange;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire.WireSystemFlagsDeployResult.WireOperationFailure;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire.WireSystemFlagsDeployResult.WireWarning;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.BiFunction;
-import java.util.function.Function;
-
-/**
- * @author bjorncs
- */
-class SystemFlagsDeployResult {
-
- private final List<FlagDataChange> flagChanges;
- private final List<OperationError> errors;
- private final List<Warning> warnings;
-
- SystemFlagsDeployResult(List<FlagDataChange> flagChanges, List<OperationError> errors, List<Warning> warnings) {
- this.flagChanges = flagChanges;
- this.errors = errors;
- this.warnings = warnings;
- }
-
- SystemFlagsDeployResult(List<OperationError> errors) {
- this(List.of(), errors, List.of());
- }
-
- List<FlagDataChange> flagChanges() {
- return flagChanges;
- }
-
- List<OperationError> errors() {
- return errors;
- }
-
- List<Warning> warnings() { return warnings; }
-
- static SystemFlagsDeployResult merge(List<SystemFlagsDeployResult> results) {
- List<FlagDataChange> mergedChanges = mergeChanges(results);
- List<OperationError> mergedErrors = mergeErrors(results);
- List<Warning> mergedWarnings = mergeWarnings(results);
- return new SystemFlagsDeployResult(mergedChanges, mergedErrors, mergedWarnings);
- }
-
- private static List<OperationError> mergeErrors(List<SystemFlagsDeployResult> results) {
- return merge(results, SystemFlagsDeployResult::errors, OperationError::targets,
- OperationErrorWithoutTarget::new, OperationErrorWithoutTarget::toOperationError);
- }
-
- private static List<FlagDataChange> mergeChanges(List<SystemFlagsDeployResult> results) {
- return merge(results, SystemFlagsDeployResult::flagChanges, FlagDataChange::targets,
- FlagDataChangeWithoutTarget::new, FlagDataChangeWithoutTarget::toFlagDataChange);
- }
-
- private static List<Warning> mergeWarnings(List<SystemFlagsDeployResult> results) {
- return merge(results, SystemFlagsDeployResult::warnings, Warning::targets,
- WarningWithoutTarget::new, WarningWithoutTarget::toWarning);
- }
-
- private static <VALUE, VALUE_WITHOUT_TARGET> List<VALUE> merge(
- List<SystemFlagsDeployResult> results,
- Function<SystemFlagsDeployResult, List<VALUE>> valuesGetter,
- Function<VALUE, Set<FlagsTarget>> targetsGetter,
- Function<VALUE, VALUE_WITHOUT_TARGET> transformer,
- BiFunction<VALUE_WITHOUT_TARGET, Set<FlagsTarget>, VALUE> reverseTransformer) {
- Map<VALUE_WITHOUT_TARGET, Set<FlagsTarget>> targetsForValue = new HashMap<>();
- for (SystemFlagsDeployResult result : results) {
- for (VALUE value : valuesGetter.apply(result)) {
- VALUE_WITHOUT_TARGET valueWithoutTarget = transformer.apply(value);
- targetsForValue.computeIfAbsent(valueWithoutTarget, k -> new HashSet<>())
- .addAll(targetsGetter.apply(value));
- }
- }
- List<VALUE> mergedValues = new ArrayList<>();
- targetsForValue.forEach(
- (value, targets) -> mergedValues.add(reverseTransformer.apply(value, targets)));
- return mergedValues;
- }
-
- WireSystemFlagsDeployResult toWire() {
- var wireResult = new WireSystemFlagsDeployResult();
- wireResult.changes = new ArrayList<>();
- for (FlagDataChange change : flagChanges) {
- var wireChange = new WireFlagDataChange();
- wireChange.flagId = change.flagId().toString();
- wireChange.owners = owners(change.flagId());
- wireChange.operation = change.operation().asString();
- wireChange.targets = change.targets().stream().map(FlagsTarget::asString).toList();
- wireChange.data = change.data().map(FlagData::toWire).orElse(null);
- wireChange.previousData = change.previousData().map(FlagData::toWire).orElse(null);
- wireResult.changes.add(wireChange);
- }
- wireResult.errors = new ArrayList<>();
- for (OperationError error : errors) {
- var wireError = new WireOperationFailure();
- wireError.message = error.message();
- wireError.operation = error.operation().asString();
- wireError.targets = error.targets().stream().map(FlagsTarget::asString).toList();
- wireError.flagId = error.flagId().map(FlagId::toString).orElse(null);
- wireError.owners = error.flagId().map(id -> owners(id)).orElse(List.of());
- wireError.data = error.flagData().map(FlagData::toWire).orElse(null);
- wireResult.errors.add(wireError);
- }
- wireResult.warnings = new ArrayList<>();
- for (Warning warning : warnings) {
- var wireWarning = new WireWarning();
- wireWarning.message = warning.message();
- wireWarning.flagId = warning.flagId().toString();
- wireWarning.owners = owners(warning.flagId());
- wireWarning.targets = warning.targets().stream().map(FlagsTarget::asString).toList();
- wireResult.warnings.add(wireWarning);
- }
- return wireResult;
- }
-
- private static List<String> owners(FlagId id) {
- return Flags.getFlag(id).map(FlagDefinition::getOwners).orElse(List.of());
- }
-
- static class FlagDataChange {
-
- private final FlagId flagId;
- private final Set<FlagsTarget> targets;
- private final OperationType operationType;
- private final FlagData data;
- private final FlagData previousData;
-
- private FlagDataChange(
- FlagId flagId, Set<FlagsTarget> targets, OperationType operationType, FlagData data, FlagData previousData) {
- this.flagId = flagId;
- this.targets = targets;
- this.operationType = operationType;
- this.data = data;
- this.previousData = previousData;
- }
-
- static FlagDataChange created(FlagId flagId, FlagsTarget target, FlagData data) {
- return new FlagDataChange(flagId, Set.of(target), OperationType.CREATE, data, null);
- }
-
- static FlagDataChange deleted(FlagId flagId, FlagsTarget target) {
- return new FlagDataChange(flagId, Set.of(target), OperationType.DELETE, null, null);
- }
-
- static FlagDataChange updated(FlagId flagId, FlagsTarget target, FlagData data, FlagData previousData) {
- return new FlagDataChange(flagId, Set.of(target), OperationType.UPDATE, data, previousData);
- }
-
- FlagId flagId() {
- return flagId;
- }
-
- Set<FlagsTarget> targets() {
- return targets;
- }
-
- OperationType operation() {
- return operationType;
- }
-
- Optional<FlagData> data() {
- return Optional.ofNullable(data);
- }
-
- Optional<FlagData> previousData() {
- return Optional.ofNullable(previousData);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- FlagDataChange that = (FlagDataChange) o;
- return Objects.equals(flagId, that.flagId) &&
- Objects.equals(targets, that.targets) &&
- operationType == that.operationType &&
- Objects.equals(data, that.data) &&
- Objects.equals(previousData, that.previousData);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(flagId, targets, operationType, data, previousData);
- }
-
- @Override
- public String toString() {
- return "FlagDataChange{" +
- "flagId=" + flagId +
- ", targets=" + targets +
- ", operationType=" + operationType +
- ", data=" + data +
- ", previousData=" + previousData +
- '}';
- }
- }
-
- static class OperationError {
-
- final String message;
- final Set<FlagsTarget> targets;
- final OperationType operation;
- final FlagId flagId;
- final FlagData flagData;
-
- private OperationError(
- String message, Set<FlagsTarget> targets, OperationType operation, FlagId flagId, FlagData flagData) {
- this.message = message;
- this.targets = targets;
- this.operation = operation;
- this.flagId = flagId;
- this.flagData = flagData;
- }
-
- static OperationError listFailed(String message, FlagsTarget target) {
- return new OperationError(message, Set.of(target), OperationType.LIST, null, null);
- }
-
- static OperationError createFailed(String message, FlagsTarget target, FlagData flagData) {
- return new OperationError(message, Set.of(target), OperationType.CREATE, flagData.id(), flagData);
- }
-
- static OperationError updateFailed(String message, FlagsTarget target, FlagData flagData) {
- return new OperationError(message, Set.of(target), OperationType.UPDATE, flagData.id(), flagData);
- }
-
- static OperationError deleteFailed(String message, FlagsTarget target, FlagId id) {
- return new OperationError(message, Set.of(target), OperationType.DELETE, id, null);
- }
-
- static OperationError archiveValidationFailed(String message) {
- return new OperationError(message, Set.of(), OperationType.VALIDATE_ARCHIVE, null, null);
- }
-
- static OperationError dataForUndefinedFlag(FlagsTarget target, FlagId id) {
- return new OperationError("Flag data present for undefined flag. Remove flag data files if flag's definition " +
- "is already removed from Flags / PermanentFlags. Consult ModelContext.FeatureFlags " +
- "for safe removal of flag used by config-model.",
- Set.of(), OperationType.DATA_FOR_UNDEFINED_FLAG, id, null);
- }
-
- String message() { return message; }
- Set<FlagsTarget> targets() { return targets; }
- OperationType operation() { return operation; }
- Optional<FlagId> flagId() { return Optional.ofNullable(flagId); }
- Optional<FlagData> flagData() { return Optional.ofNullable(flagData); }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- OperationError that = (OperationError) o;
- return Objects.equals(message, that.message) &&
- Objects.equals(targets, that.targets) &&
- operation == that.operation &&
- Objects.equals(flagId, that.flagId) &&
- Objects.equals(flagData, that.flagData);
- }
-
- @Override public int hashCode() { return Objects.hash(message, targets, operation, flagId, flagData); }
-
- @Override
- public String toString() {
- return "OperationFailure{" +
- "message='" + message + '\'' +
- ", targets=" + targets +
- ", operation=" + operation +
- ", flagId=" + flagId +
- ", flagData=" + flagData +
- '}';
- }
- }
-
- enum OperationType {
- CREATE("create"), DELETE("delete"), UPDATE("update"), LIST("list"), VALIDATE_ARCHIVE("validate-archive"),
- DATA_FOR_UNDEFINED_FLAG("data-for-undefined-flag");
-
- private final String stringValue;
-
- OperationType(String stringValue) { this.stringValue = stringValue; }
-
- String asString() { return stringValue; }
- }
-
- static class Warning {
- final String message;
- final Set<FlagsTarget> targets;
- final FlagId flagId;
-
- private Warning(String message, Set<FlagsTarget> targets, FlagId flagId) {
- this.message = message;
- this.targets = targets;
- this.flagId = flagId;
- }
-
- static Warning dataForUndefinedFlag(FlagsTarget target, FlagId flagId) {
- return new Warning(
- "Flag data present for undefined flag. Remove flag data files if flag's definition is already removed from Flags/PermanentFlags. " +
- "Consult ModelContext.FeatureFlags for safe removal of flag used by config-model.", Set.of(target), flagId);
- }
-
- String message() { return message; }
- Set<FlagsTarget> targets() { return targets; }
- FlagId flagId() { return flagId; }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Warning warning = (Warning) o;
- return Objects.equals(message, warning.message) &&
- Objects.equals(targets, warning.targets) &&
- Objects.equals(flagId, warning.flagId);
- }
-
- @Override public int hashCode() { return Objects.hash(message, targets, flagId); }
- }
-
- private static class FlagDataChangeWithoutTarget {
- final FlagId flagId;
- final OperationType operationType;
- final FlagData data;
- final FlagData previousData;
- final JsonNode jsonData; // needed for FlagData equality check
- final JsonNode jsonPreviousData; // needed for FlagData equality check
-
-
- FlagDataChangeWithoutTarget(FlagDataChange change) {
- this.flagId = change.flagId();
- this.operationType = change.operation();
- this.data = change.data().orElse(null);
- this.previousData = change.previousData().orElse(null);
- this.jsonData = Optional.ofNullable(data).map(FlagData::toJsonNode).orElse(null);
- this.jsonPreviousData = Optional.ofNullable(previousData).map(FlagData::toJsonNode).orElse(null);
- }
-
- FlagDataChange toFlagDataChange(Set<FlagsTarget> targets) {
- return new FlagDataChange(flagId, targets, operationType, data, previousData);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- FlagDataChangeWithoutTarget that = (FlagDataChangeWithoutTarget) o;
- return Objects.equals(flagId, that.flagId) &&
- operationType == that.operationType &&
- Objects.equals(jsonData, that.jsonData) &&
- Objects.equals(jsonPreviousData, that.jsonPreviousData);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(flagId, operationType, jsonData, jsonPreviousData);
- }
- }
-
- private static class OperationErrorWithoutTarget {
- final String message;
- final OperationType operation;
- final FlagId flagId;
- final FlagData flagData;
-
- OperationErrorWithoutTarget(OperationError operationError) {
- this.message = operationError.message();
- this.operation = operationError.operation();
- this.flagId = operationError.flagId().orElse(null);
- this.flagData = operationError.flagData().orElse(null);
- }
-
- OperationError toOperationError(Set<FlagsTarget> targets) {
- return new OperationError(message, targets, operation, flagId, flagData);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- OperationErrorWithoutTarget that = (OperationErrorWithoutTarget) o;
- return Objects.equals(message, that.message) &&
- operation == that.operation &&
- Objects.equals(flagId, that.flagId) &&
- Objects.equals(flagData, that.flagData);
- }
-
- @Override public int hashCode() { return Objects.hash(message, operation, flagId, flagData); }
- }
-
- private static class WarningWithoutTarget {
- final String message;
- final FlagId flagId;
-
- WarningWithoutTarget(Warning warning) {
- this.message = warning.message();
- this.flagId = warning.flagId();
- }
-
- Warning toWarning(Set<FlagsTarget> targets) { return new Warning(message, targets, flagId); }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- WarningWithoutTarget that = (WarningWithoutTarget) o;
- return Objects.equals(message, that.message) &&
- Objects.equals(flagId, that.flagId);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(message, flagId);
- }
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java
deleted file mode 100644
index 0fa800e7367..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployer.java
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.systemflags;
-
-import com.yahoo.concurrent.DaemonThreadFactory;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.flags.FlagId;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.hosted.controller.api.integration.ControllerIdentityProvider;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagValidationException;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.SystemFlagsDataArchive;
-import com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.OperationError;
-import com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.Warning;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.function.Function;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.FlagDataChange;
-
-/**
- * Deploy a flags data archive to all targets in a given system
- *
- * @author bjorncs
- */
-class SystemFlagsDeployer {
-
- private static final Logger log = Logger.getLogger(SystemFlagsDeployer.class.getName());
-
- private final FlagsClient client;
- private final SystemName system;
- private final Set<FlagsTarget> targets;
- private final ExecutorService executor = Executors.newCachedThreadPool(new DaemonThreadFactory("system-flags-deployer-"));
-
-
- SystemFlagsDeployer(ControllerIdentityProvider identityProvider, SystemName system, Set<FlagsTarget> targets) {
- this(new FlagsClient(identityProvider, targets), system, targets);
- }
-
- SystemFlagsDeployer(FlagsClient client, SystemName system, Set<FlagsTarget> targets) {
- this.client = client;
- this.system = system;
- this.targets = targets;
- }
-
- SystemFlagsDeployResult deployFlags(SystemFlagsDataArchive archive, boolean dryRun) {
- try {
- archive.validateAllFilesAreForTargets(targets);
- } catch (FlagValidationException e) {
- return new SystemFlagsDeployResult(List.of(OperationError.archiveValidationFailed(e.getMessage())));
- }
-
- Map<FlagsTarget, Future<SystemFlagsDeployResult>> futures = new HashMap<>();
- for (FlagsTarget target : targets) {
- futures.put(target, executor.submit(() -> deployFlags(target, archive.flagData(target), dryRun)));
- }
- List<SystemFlagsDeployResult> results = new ArrayList<>();
- futures.forEach((target, future) -> {
- try {
- results.add(future.get());
- } catch (InterruptedException | ExecutionException e) {
- log.log(Level.SEVERE, Text.format("Failed to deploy flags for target '%s': %s", target, e.getMessage()), e);
- throw new RuntimeException(e);
- }
- });
- return SystemFlagsDeployResult.merge(results);
- }
-
- private SystemFlagsDeployResult deployFlags(FlagsTarget target, List<FlagData> flagDataList, boolean dryRun) {
- flagDataList = flagDataList.stream()
- .map(target::partiallyResolveFlagData)
- .filter(flagData -> !flagData.isEmpty())
- .toList();
- Map<FlagId, FlagData> wantedFlagData = lookupTable(flagDataList);
- Map<FlagId, FlagData> currentFlagData;
- List<FlagId> definedFlags;
- try {
- currentFlagData = lookupTable(client.listFlagData(target));
- definedFlags = client.listDefinedFlags(target);
- } catch (Exception e) {
- log.log(Level.WARNING, Text.format("Failed to list flag data for target '%s': %s", target, e.getMessage()), e);
- return new SystemFlagsDeployResult(List.of(OperationError.listFailed(e.getMessage(), target)));
- }
-
- List<OperationError> errors = new ArrayList<>();
- List<FlagDataChange> results = new ArrayList<>();
- List<Warning> warnings = new ArrayList<>();
-
- createNewFlagData(target, dryRun, wantedFlagData, currentFlagData, results, errors);
- updateExistingFlagData(target, dryRun, wantedFlagData, currentFlagData, results, errors);
- removeOldFlagData(target, dryRun, wantedFlagData, currentFlagData, results, errors);
- failOnNewFlagDataForUndefinedFlags(target, wantedFlagData, currentFlagData, definedFlags, errors);
- failOnFlagDataForUndefinedFlags(target, wantedFlagData, currentFlagData, definedFlags, errors);
- return new SystemFlagsDeployResult(results, errors, warnings);
- }
-
- private void createNewFlagData(FlagsTarget target,
- boolean dryRun,
- Map<FlagId, FlagData> wantedFlagData,
- Map<FlagId, FlagData> currentFlagData,
- List<FlagDataChange> results,
- List<OperationError> errors) {
- wantedFlagData.forEach((id, data) -> {
- FlagData currentData = currentFlagData.get(id);
- if (currentData != null) {
- return; // not a new flag
- }
- try {
- if (!dryRun) {
- client.putFlagData(target, data);
- } else {
- dryRunFlagDataValidation(data);
- }
- } catch (Exception e) {
- log.log(Level.WARNING, Text.format("Failed to put flag '%s' for target '%s': %s", data.id(), target, e.getMessage()), e);
- errors.add(OperationError.createFailed(e.getMessage(), target, data));
- return;
- }
- results.add(FlagDataChange.created(id, target, data));
- });
- }
-
- private void updateExistingFlagData(FlagsTarget target,
- boolean dryRun,
- Map<FlagId, FlagData> wantedFlagData,
- Map<FlagId, FlagData> currentFlagData,
- List<FlagDataChange> results,
- List<OperationError> errors) {
- wantedFlagData.forEach((id, wantedData) -> {
- FlagData currentData = currentFlagData.get(id);
- if (currentData == null || isEqual(currentData, wantedData)) {
- return; // not an flag data update
- }
- try {
- if (!dryRun) {
- client.putFlagData(target, wantedData);
- } else {
- dryRunFlagDataValidation(wantedData);
- }
- } catch (Exception e) {
- log.log(Level.WARNING, Text.format("Failed to update flag '%s' for target '%s': %s", wantedData.id(), target, e.getMessage()), e);
- errors.add(OperationError.updateFailed(e.getMessage(), target, wantedData));
- return;
- }
- results.add(FlagDataChange.updated(id, target, wantedData, currentData));
- });
- }
-
- private void removeOldFlagData(FlagsTarget target,
- boolean dryRun,
- Map<FlagId, FlagData> wantedFlagData,
- Map<FlagId, FlagData> currentFlagData,
- List<FlagDataChange> results,
- List<OperationError> errors) {
- currentFlagData.forEach((id, data) -> {
- if (wantedFlagData.containsKey(id)) {
- return; // not a removed flag
- }
- if (!dryRun) {
- try {
- client.deleteFlagData(target, id);
- } catch (Exception e) {
- log.log(Level.WARNING, Text.format("Failed to delete flag '%s' for target '%s': %s", id, target, e.getMessage()), e);
- errors.add(OperationError.deleteFailed(e.getMessage(), target, id));
- return;
- }
- }
- results.add(FlagDataChange.deleted(id, target));
- });
- }
-
- private static void failOnNewFlagDataForUndefinedFlags(FlagsTarget target,
- Map<FlagId, FlagData> wantedFlagData,
- Map<FlagId, FlagData> currentFlagData,
- List<FlagId> definedFlags,
- List<OperationError> errors) {
- String errorMessage = "Flag not defined in target zone. If zone/configserver cluster is new, add an empty flag " +
- "data file for this zone as a temporary measure until the stale flag data files are removed.";
- for (FlagId flagId : wantedFlagData.keySet()) {
- if (!currentFlagData.containsKey(flagId) && !definedFlags.contains(flagId)) {
- errors.add(OperationError.createFailed(errorMessage, target, wantedFlagData.get(flagId)));
- }
- }
- }
-
- private static void failOnFlagDataForUndefinedFlags(FlagsTarget target,
- Map<FlagId, FlagData> wantedFlagData,
- Map<FlagId, FlagData> currentFlagData,
- List<FlagId> definedFlags,
- List<OperationError> errors) {
- for (FlagId flagId : currentFlagData.keySet()) {
- if (wantedFlagData.containsKey(flagId) && !definedFlags.contains(flagId)) {
- errors.add(OperationError.dataForUndefinedFlag(target, flagId));
- }
- }
- }
-
- private static void dryRunFlagDataValidation(FlagData data) {
- Flags.getFlag(data.id())
- .ifPresent(definition -> data.validate(definition.getUnboundFlag().serializer()));
- }
-
- private static Map<FlagId, FlagData> lookupTable(Collection<FlagData> data) {
- return data.stream().collect(Collectors.toMap(FlagData::id, Function.identity()));
- }
-
- private static boolean isEqual(FlagData l, FlagData r) {
- return Objects.equals(l.toJsonNode(), r.toJsonNode());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java
deleted file mode 100644
index 6318dc8c6fa..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsHandler.java
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.systemflags;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.JacksonJsonResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.vespa.hosted.controller.api.integration.ControllerIdentityProvider;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagValidationException;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.SystemFlagsDataArchive;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-
-import java.io.InputStream;
-import java.util.List;
-import java.util.concurrent.Executor;
-
-/**
- * Handler implementation for '/system-flags/v1', an API for controlling system-wide feature flags
- *
- * @author bjorncs
- */
-@SuppressWarnings("unused") // Request handler listed in controller's services.xml
-public class SystemFlagsHandler extends ThreadedHttpRequestHandler {
-
- private static final String API_PREFIX = "/system-flags/v1";
-
- private final SystemFlagsDeployer deployer;
- private final ZoneRegistry zoneRegistry;
-
- @Inject
- public SystemFlagsHandler(ZoneRegistry zoneRegistry,
- ControllerIdentityProvider identityProvider,
- Executor executor) {
- super(executor);
- this.zoneRegistry = zoneRegistry;
- this.deployer = new SystemFlagsDeployer(identityProvider, zoneRegistry.system(), FlagsTarget.getAllTargetsInSystem(zoneRegistry, true));
- }
-
- @Override
- public HttpResponse handle(HttpRequest request) {
- return switch (request.getMethod()) {
- case PUT -> put(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
- };
- }
-
- private HttpResponse put(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches(API_PREFIX + "/deploy")) return deploy(request, /*dryRun*/false);
- if (path.matches(API_PREFIX + "/dryrun")) return deploy(request, /*dryRun*/true);
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private HttpResponse deploy(HttpRequest request, boolean dryRun) {
- try {
- String contentType = request.getHeader("Content-Type");
- if (!contentType.equalsIgnoreCase("application/zip")) {
- return ErrorResponse.badRequest("Invalid content type: " + contentType);
- }
- SystemFlagsDeployResult result = deploy(request.getData(), dryRun);
- return new JacksonJsonResponse<>(200, result.toWire());
- } catch (Exception e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private SystemFlagsDeployResult deploy(InputStream zipStream, boolean dryRun) {
- SystemFlagsDataArchive archive;
- try {
- archive = SystemFlagsDataArchive.fromZip(zipStream, zoneRegistry);
- } catch (FlagValidationException e) {
- return new SystemFlagsDeployResult(List.of(SystemFlagsDeployResult.OperationError.archiveValidationFailed(e.getMessage())));
- }
-
- return deployer.deployFlags(archive, dryRun);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
deleted file mode 100644
index 11a5e178703..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiHandler.java
+++ /dev/null
@@ -1,417 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.user;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.container.jdisc.EmptyResponse;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.io.IOUtils;
-import com.yahoo.jdisc.http.filter.security.misc.User;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.MessageResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeStream;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.configserver.flags.FlagsDb;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.IntFlag;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Plan;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.user.Roles;
-import com.yahoo.vespa.hosted.controller.api.integration.user.UserId;
-import com.yahoo.vespa.hosted.controller.api.integration.user.UserManagement;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.RoleDefinition;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.yolean.Exceptions;
-
-import java.security.PublicKey;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * API for user management related to access control.
- *
- * @author jonmv
- */
-@SuppressWarnings("unused") // Handler
-public class UserApiHandler extends ThreadedHttpRequestHandler {
-
- private final static Logger log = Logger.getLogger(UserApiHandler.class.getName());
-
- private final UserManagement users;
- private final Controller controller;
- private final FlagsDb flagsDb;
- private final IntFlag maxTrialTenants;
-
- @Inject
- public UserApiHandler(Context parentCtx, UserManagement users, Controller controller, FlagSource flagSource, FlagsDb flagsDb) {
- super(parentCtx);
- this.users = users;
- this.controller = controller;
- this.flagsDb = flagsDb;
- this.maxTrialTenants = PermanentFlags.MAX_TRIAL_TENANTS.bindTo(flagSource);
- }
-
- @Override
- public HttpResponse handle(HttpRequest request) {
- try {
- Path path = new Path(request.getUri());
- switch (request.getMethod()) {
- case GET: return handleGET(path, request);
- case POST: return handlePOST(path, request);
- case DELETE: return handleDELETE(path, request);
- case OPTIONS: return handleOPTIONS();
- default: return ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is not supported");
- }
- }
- catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- }
- catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse handleGET(Path path, HttpRequest request) {
- if (path.matches("/user/v1/user")) return userMetadata(request);
- if (path.matches("/user/v1/find")) return findUser(request);
- if (path.matches("/user/v1/tenant/{tenant}")) return listTenantRoleMembers(path.get("tenant"));
- if (path.matches("/user/v1/tenant/{tenant}/application/{application}")) return listApplicationRoleMembers(path.get("tenant"), path.get("application"));
-
- return ErrorResponse.notFoundError(Text.format("No '%s' handler at '%s'", request.getMethod(),
- request.getUri().getPath()));
- }
-
- private HttpResponse handlePOST(Path path, HttpRequest request) {
- if (path.matches("/user/v1/tenant/{tenant}")) return addTenantRoleMember(path.get("tenant"), request);
- if (path.matches("/user/v1/email/verify")) return verifyEmail(request);
-
- return ErrorResponse.notFoundError(Text.format("No '%s' handler at '%s'", request.getMethod(),
- request.getUri().getPath()));
- }
-
- private HttpResponse handleDELETE(Path path, HttpRequest request) {
- if (path.matches("/user/v1/tenant/{tenant}")) return removeTenantRoleMember(path.get("tenant"), request);
-
- return ErrorResponse.notFoundError(Text.format("No '%s' handler at '%s'", request.getMethod(),
- request.getUri().getPath()));
- }
-
- private HttpResponse handleOPTIONS() {
- EmptyResponse response = new EmptyResponse();
- response.headers().put("Allow", "GET,PUT,POST,PATCH,DELETE,OPTIONS");
- return response;
- }
-
- private static final Set<RoleDefinition> hostedOperators = Set.of(
- RoleDefinition.hostedOperator,
- RoleDefinition.hostedSupporter,
- RoleDefinition.hostedAccountant);
-
- private HttpResponse findUser(HttpRequest request) {
- var email = request.getProperty("email");
- var query = request.getProperty("query");
- if (email != null) return userMetadataFromUserId(email);
- if (query != null) return userMetadataQuery(query);
- return ErrorResponse.badRequest("Need 'email' or 'query' parameter");
- }
-
- private HttpResponse userMetadataFromUserId(String email) {
- var maybeUser = users.findUser(email);
-
- var slime = new Slime();
- var root = slime.setObject();
- var usersRoot = root.setArray("users");
-
- if (maybeUser.isPresent()) {
- var user = maybeUser.get();
- var roles = users.listRoles(new UserId(user.email()));
- renderUserMetaData(usersRoot.addObject(), user, Set.copyOf(roles));
- }
-
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse userMetadataQuery(String query) {
- var userList = users.findUsers(query);
-
- var slime = new Slime();
- var root = slime.setObject();
- var userSlime = root.setArray("users");
-
- for (var user : userList) {
- var roles = users.listRoles(new UserId((user.email())));
- renderUserMetaData(userSlime.addObject(), user, Set.copyOf(roles));
- }
-
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse userMetadata(HttpRequest request) {
- User user;
- if (request.getJDiscRequest().context().get(User.ATTRIBUTE_NAME) instanceof User) {
- user = getAttribute(request, User.ATTRIBUTE_NAME, User.class);
- } else {
- // Remove this after June 2021 (once all security filters are setting this)
- @SuppressWarnings("unchecked")
- Map<String, String> attr = (Map<String, String>) getAttribute(request, User.ATTRIBUTE_NAME, Map.class);
- user = new User(attr.get("email"), attr.get("name"), attr.get("nickname"), attr.get("picture"));
- }
-
- Set<Role> roles = getAttribute(request, SecurityContext.ATTRIBUTE_NAME, SecurityContext.class).roles();
-
- var slime = new Slime();
- renderUserMetaData(slime.setObject(), user, roles);
- return new SlimeJsonResponse(slime);
- }
-
- private void renderUserMetaData(Cursor root, User user, Set<Role> roles) {
- Map<TenantName, List<TenantRole>> tenantRolesByTenantName = roles.stream()
- .flatMap(role -> filterTenantRoles(role).stream())
- .distinct()
- .sorted(Comparator.comparing(Role::definition).reversed())
- .collect(Collectors.groupingBy(TenantRole::tenant, Collectors.toList()));
-
- // List of operator roles as defined in `hostedOperators` above
- List<Role> operatorRoles = roles.stream()
- .filter(role -> hostedOperators.contains(role.definition()))
- .sorted(Comparator.comparing(Role::definition))
- .toList();
-
- root.setBool("isPublic", controller.system().isPublic());
- root.setBool("isCd", controller.system().isCd());
- root.setBool("hasTrialCapacity", hasTrialCapacity());
-
- toSlime(root.setObject("user"), user);
-
- Cursor tenants = root.setObject("tenants");
- tenantRolesByTenantName.keySet().stream()
- .sorted()
- .forEach(tenant -> {
- Cursor tenantObject = tenants.setObject(tenant.value());
- tenantObject.setBool("supported", hasSupportedPlan(tenant));
-
- Cursor tenantRolesObject = tenantObject.setArray("roles");
- tenantRolesByTenantName.getOrDefault(tenant, List.of())
- .forEach(role -> tenantRolesObject.addString(role.definition().name()));
- });
-
- if (!operatorRoles.isEmpty()) {
- Cursor operator = root.setArray("operator");
- operatorRoles.forEach(role -> operator.addString(role.definition().name()));
- }
-
- UserFlagsSerializer.toSlime(root, flagsDb.getAllFlagData(), tenantRolesByTenantName.keySet(), !operatorRoles.isEmpty(), user.email());
- }
-
- private HttpResponse listTenantRoleMembers(String tenantName) {
- if (controller.tenants().get(tenantName).isPresent()) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- root.setString("tenant", tenantName);
- fillRoles(root,
- Roles.tenantRoles(TenantName.from(tenantName)),
- Collections.emptyList());
- return new SlimeJsonResponse(slime);
- }
- return ErrorResponse.notFoundError("Tenant '" + tenantName + "' does not exist");
- }
-
- private HttpResponse listApplicationRoleMembers(String tenantName, String applicationName) {
- var id = TenantAndApplicationId.from(tenantName, applicationName);
- if (controller.applications().getApplication(id).isPresent()) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- root.setString("tenant", tenantName);
- root.setString("application", applicationName);
- fillRoles(root,
- Roles.applicationRoles(TenantName.from(tenantName), ApplicationName.from(applicationName)),
- Roles.tenantRoles(TenantName.from(tenantName)));
- return new SlimeJsonResponse(slime);
- }
- return ErrorResponse.notFoundError("Application '" + id + "' does not exist");
- }
-
- private void fillRoles(Cursor root, List<? extends Role> roles, List<? extends Role> superRoles) {
- Cursor rolesArray = root.setArray("roleNames");
- for (Role role : roles)
- rolesArray.addString(valueOf(role));
-
- Map<User, List<Role>> memberships = new LinkedHashMap<>();
- List<Role> allRoles = new ArrayList<>(superRoles); // Membership in a super role may imply membership in a role.
- allRoles.addAll(roles);
- for (Role role : allRoles)
- for (User user : users.listUsers(role)) {
- memberships.putIfAbsent(user, new ArrayList<>());
- memberships.get(user).add(role);
- }
-
- Cursor usersArray = root.setArray("users");
- memberships.forEach((user, userRoles) -> {
- Cursor userObject = usersArray.addObject();
- toSlime(userObject, user);
-
- Cursor rolesObject = userObject.setObject("roles");
- for (Role role : roles) {
- Cursor roleObject = rolesObject.setObject(valueOf(role));
- roleObject.setBool("explicit", userRoles.contains(role));
- roleObject.setBool("implied", userRoles.stream().anyMatch(userRole -> userRole.implies(role)));
- }
- });
- }
-
- private static void toSlime(Cursor userObject, User user) {
- if (user.name() != null) userObject.setString("name", user.name());
- userObject.setString("email", user.email());
- if (user.nickname() != null) userObject.setString("nickname", user.nickname());
- if (user.picture() != null) userObject.setString("picture", user.picture());
- userObject.setBool("verified", user.isVerified());
- if (!user.lastLogin().equals(User.NO_DATE))
- userObject.setString("lastLogin", user.lastLogin().format(DateTimeFormatter.ISO_DATE));
- if (user.loginCount() > -1)
- userObject.setLong("loginCount", user.loginCount());
- }
-
- private HttpResponse addTenantRoleMember(String tenantName, HttpRequest request) {
- Inspector requestObject = bodyInspector(request);
- var tenant = TenantName.from(tenantName);
- var user = new UserId(require("user", Inspector::asString, requestObject));
- var roles = SlimeStream.fromArray(requestObject.field("roles"), Inspector::asString)
- .map(roleName -> Roles.toRole(tenant, roleName))
- .toList();
-
- users.addToRoles(user, roles);
- return new MessageResponse(user + " is now a member of " + roles.stream().map(Role::toString).collect(Collectors.joining(", ")));
- }
-
- private HttpResponse verifyEmail(HttpRequest request) {
- var inspector = bodyInspector(request);
- var verificationCode = require("verificationCode", Inspector::asString, inspector);
- var verified = controller.mailVerifier().verifyMail(verificationCode);
-
- if (verified)
- return new MessageResponse("Email with verification code " + verificationCode + " has been verified");
- return ErrorResponse.notFoundError("No pending email verification with code " + verificationCode + " found");
- }
-
- private HttpResponse removeTenantRoleMember(String tenantName, HttpRequest request) {
- Inspector requestObject = bodyInspector(request);
- var tenant = TenantName.from(tenantName);
- var user = new UserId(require("user", Inspector::asString, requestObject));
- var roles = SlimeStream.fromArray(requestObject.field("roles"), Inspector::asString)
- .map(roleName -> Roles.toRole(tenant, roleName))
- .toList();
-
- enforceLastAdminOfTenant(tenant, user, roles);
- removeDeveloperKey(tenant, user, roles);
- users.removeFromRoles(user, roles);
-
- controller.tenants().lockIfPresent(tenant, LockedTenant.class, lockedTenant -> {
- if (lockedTenant instanceof LockedTenant.Cloud cloudTenant)
- controller.tenants().store(cloudTenant.withInvalidateUserSessionsBefore(controller.clock().instant()));
- });
-
- return new MessageResponse(user + " is no longer a member of " + roles.stream().map(Role::toString).collect(Collectors.joining(", ")));
- }
-
- private void enforceLastAdminOfTenant(TenantName tenantName, UserId user, List<Role> roles) {
- for (Role role : roles) {
- if (role.definition().equals(RoleDefinition.administrator)) {
- if (Set.of(user.value()).equals(users.listUsers(role).stream().map(User::email).collect(Collectors.toSet()))) {
- throw new IllegalArgumentException("Can't remove the last administrator of a tenant.");
- }
- break;
- }
- }
- }
-
- private void removeDeveloperKey(TenantName tenantName, UserId user, List<Role> roles) {
- for (Role role : roles) {
- if (role.definition().equals(RoleDefinition.developer)) {
- controller.tenants().lockIfPresent(tenantName, LockedTenant.Cloud.class, tenant -> {
- PublicKey key = tenant.get().developerKeys().inverse().get(new SimplePrincipal(user.value()));
- if (key != null)
- controller.tenants().store(tenant.withoutDeveloperKey(key));
- });
- break;
- }
- }
- }
-
- private boolean hasTrialCapacity() {
- if (! controller.system().isPublic()) return true;
- var plan = controller.serviceRegistry().planRegistry().plan("trial");
- return controller.serviceRegistry().billingController().tenantsWithPlanUnderLimit(plan.get(), maxTrialTenants.value());
- }
-
- private static Inspector bodyInspector(HttpRequest request) {
- return Exceptions.uncheck(() -> SlimeUtils.jsonToSlime(IOUtils.readBytes(request.getData(), 1 << 10)).get());
- }
-
- private static <Type> Type require(String name, Function<Inspector, Type> mapper, Inspector object) {
- if ( ! object.field(name).valid()) throw new IllegalArgumentException("Missing field '" + name + "'.");
- return mapper.apply(object.field(name));
- }
-
- private static String valueOf(Role role) {
- switch (role.definition()) {
- case administrator: return "administrator";
- case developer: return "developer";
- case reader: return "reader";
- case headless: return "headless";
- default: throw new IllegalArgumentException("Unexpected role type '" + role.definition() + "'.");
- }
- }
-
- private static Collection<TenantRole> filterTenantRoles(Role role) {
- if (role instanceof TenantRole tenantRole) {
- switch (tenantRole.definition()) {
- case administrator, developer, reader, hostedDeveloper: return Set.of(tenantRole);
- case athenzTenantAdmin: return Roles.tenantRoles(tenantRole.tenant());
- }
- }
- return Set.of();
- }
-
- private static <T> T getAttribute(HttpRequest request, String attributeName, Class<T> clazz) {
- return Optional.ofNullable(request.getJDiscRequest().context().get(attributeName))
- .filter(clazz::isInstance)
- .map(clazz::cast)
- .orElseThrow(() -> new IllegalArgumentException("Attribute '" + attributeName + "' was not set on request"));
- }
-
- private boolean hasSupportedPlan(TenantName tenantName) {
- var planId = controller.serviceRegistry().billingController().getPlan(tenantName);
- return controller.serviceRegistry().planRegistry().plan(planId)
- .map(Plan::isSupported)
- .orElse(false);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.java
deleted file mode 100644
index 46de4b7a348..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializer.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.user;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.lang.MutableBoolean;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagDefinition;
-import com.yahoo.vespa.flags.FlagId;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.RawFlag;
-import com.yahoo.vespa.flags.UnboundFlag;
-import com.yahoo.vespa.flags.json.Condition;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.flags.json.Rule;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Predicate;
-import java.util.stream.Stream;
-
-/**
- * @author freva
- */
-public class UserFlagsSerializer {
- static void toSlime(Cursor cursor, Map<FlagId, FlagData> rawFlagData,
- Set<TenantName> authorizedForTenantNames, boolean isOperator, String userEmail) {
- FetchVector resolveVector = FetchVector.fromMap(Map.of(FetchVector.Dimension.CONSOLE_USER_EMAIL, userEmail));
- List<FlagData> filteredFlagData = Flags.getAllFlags().stream()
- // Only include flags that have CONSOLE_USER_EMAIL dimension, this should be replaced with more explicit
- // 'target' annotation if/when that is added to flag definition
- .filter(fd -> fd.getDimensions().contains(FetchVector.Dimension.CONSOLE_USER_EMAIL))
- .map(FlagDefinition::getUnboundFlag)
- .map(flag -> filteredFlagData(flag, Optional.ofNullable(rawFlagData.get(flag.id())), authorizedForTenantNames, isOperator, resolveVector))
- .toList();
-
- byte[] bytes = FlagData.serializeListToUtf8Json(filteredFlagData);
- SlimeUtils.copyObject(SlimeUtils.jsonToSlime(bytes).get(), cursor);
- }
-
- private static <T> FlagData filteredFlagData(UnboundFlag<T, ?, ?> definition, Optional<FlagData> original,
- Set<TenantName> authorizedForTenantNames, boolean isOperator, FetchVector resolveVector) {
- MutableBoolean encounteredEmpty = new MutableBoolean(false);
- Optional<RawFlag> defaultValue = Optional.of(definition.serializer().serialize(definition.defaultValue()));
- // Include the original rules from flag DB and the default value from code if there is no default rule in DB
- List<Rule> rules = Stream.concat(original.stream().flatMap(fd -> fd.rules().stream()), Stream.of(new Rule(defaultValue)))
- // Exclude rules that do not match the resolveVector
- .filter(rule -> rule.partialMatch(resolveVector))
- // Re-create each rule with value explicitly set, either from DB or default from code and
- // a filtered set of conditions
- .map(rule -> new Rule(rule.getValueToApply().or(() -> defaultValue),
- rule.conditions().stream()
- .flatMap(condition -> filteredCondition(condition, authorizedForTenantNames, isOperator, resolveVector).stream())
- .toList()))
- // We can stop as soon as we hit the first rule that has no conditions
- .takeWhile(rule -> !encounteredEmpty.getAndSet(rule.conditions().isEmpty()))
- .toList();
-
- return new FlagData(definition.id(), new FetchVector(), rules);
- }
-
- private static Optional<Condition> filteredCondition(Condition condition, Set<TenantName> authorizedForTenantNames,
- boolean isOperator, FetchVector resolveVector) {
- // If the condition is one of the conditions that we resolve on the server, e.g. email, we do not need to
- // propagate it back to the user
- if (resolveVector.hasDimension(condition.dimension())) return Optional.empty();
-
- // For the other dimensions, filter the values down to an allowed subset
- switch (condition.dimension()) {
- case TENANT_ID: return valueSubset(condition, tenant -> isOperator || authorizedForTenantNames.contains(TenantName.from(tenant)));
- case INSTANCE_ID: return valueSubset(condition, appId -> isOperator || authorizedForTenantNames.stream().anyMatch(tenant -> appId.startsWith(tenant.value() + ":")));
- default: throw new IllegalArgumentException("Dimension " + condition.dimension() + " is not supported for user flags");
- }
- }
-
- private static Optional<Condition> valueSubset(Condition condition, Predicate<String> predicate) {
- Condition.CreateParams createParams = condition.toCreateParams();
- return Optional.of(createParams
- .withValues(createParams.values().stream().filter(predicate).toList())
- .createAs(condition.type()));
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java
deleted file mode 100644
index 90792e9febe..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiHandler.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.zone.v1;
-
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.yolean.Exceptions;
-
-import java.util.Comparator;
-import java.util.List;
-
-/**
- * Read-only REST API that provides information about zones in hosted Vespa (version 1)
- *
- * @author mpolden
- */
-@SuppressWarnings("unused")
-public class ZoneApiHandler extends ThreadedHttpRequestHandler {
-
- private final ZoneRegistry zoneRegistry;
-
- public ZoneApiHandler(ThreadedHttpRequestHandler.Context parentCtx, ServiceRegistry serviceRegistry) {
- super(parentCtx);
- this.zoneRegistry = serviceRegistry.zoneRegistry();
- }
-
- @Override
- public HttpResponse handle(HttpRequest request) {
- try {
- return switch (request.getMethod()) {
- case GET -> get(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
- };
- } catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- } catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/zone/v1")) {
- return root(request);
- }
- if (path.matches("/zone/v1/environment/{environment}")) {
- return environment(request, Environment.from(path.get("environment")));
- }
- if (path.matches("/zone/v1/environment/{environment}/default")) {
- return defaultRegion(request, Environment.from(path.get("environment")));
- }
- return notFound(path);
- }
-
- private HttpResponse root(HttpRequest request) {
- List<Environment> environments = zoneRegistry.zones().publiclyVisible().zones().stream()
- .map(ZoneApi::getEnvironment)
- .distinct()
- .sorted(Comparator.comparing(Environment::value))
- .toList();
- Slime slime = new Slime();
- Cursor root = slime.setArray();
- environments.forEach(environment -> {
- Cursor object = root.addObject();
- object.setString("name", environment.value());
- object.setString("url", request.getUri()
- .resolve("/zone/v1/environment/")
- .resolve(environment.value())
- .toString());
- });
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse environment(HttpRequest request, Environment environment) {
- Slime slime = new Slime();
- Cursor root = slime.setArray();
- zoneRegistry.zones().publiclyVisible().all().in(environment).zones().forEach(zone -> {
- Cursor object = root.addObject();
- object.setString("name", zone.getRegionName().value());
- object.setString("url", request.getUri()
- .resolve("/zone/v2/")
- .resolve(environment.value() + "/")
- .resolve(zone.getRegionName().value())
- .toString());
- });
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse defaultRegion(HttpRequest request, Environment environment) {
- RegionName region = zoneRegistry.getDefaultRegion(environment)
- .orElseThrow(() -> new IllegalArgumentException("No default region for environment: " + environment));
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- root.setString("name", region.value());
- root.setString("url", request.getUri()
- .resolve("/zone/v2/")
- .resolve(environment.value() + "/")
- .resolve(region.value())
- .toString());
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse notFound(Path path) {
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java
deleted file mode 100644
index c5b29dad8b9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author mpolden
- */
-package com.yahoo.vespa.hosted.controller.restapi.zone.v1;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java
deleted file mode 100644
index f29845d2476..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiHandler.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.zone.v2;
-
-import ai.vespa.http.HttpURL;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.config.provision.zone.ZoneList;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
-import com.yahoo.restapi.ErrorResponse;
-import com.yahoo.restapi.Path;
-import com.yahoo.restapi.SlimeJsonResponse;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLoggingRequestHandler;
-import com.yahoo.vespa.hosted.controller.proxy.ConfigServerRestExecutor;
-import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
-import com.yahoo.vespa.hosted.controller.restapi.ErrorResponses;
-import com.yahoo.yolean.Exceptions;
-
-/**
- * REST API for proxying requests to config servers in a given zone (version 2).
- *
- * This API does something completely different from /zone/v1, but such is the world.
- *
- * @author mpolden
- */
-@SuppressWarnings("unused")
-public class ZoneApiHandler extends AuditLoggingRequestHandler {
-
- private final ZoneRegistry zoneRegistry;
- private final ConfigServerRestExecutor proxy;
-
- public ZoneApiHandler(ThreadedHttpRequestHandler.Context parentCtx, ServiceRegistry serviceRegistry,
- ConfigServerRestExecutor proxy, Controller controller) {
- super(parentCtx, controller.auditLogger());
- this.zoneRegistry = serviceRegistry.zoneRegistry();
- this.proxy = proxy;
- }
-
- @Override
- public HttpResponse auditAndHandle(HttpRequest request) {
- try {
- return switch (request.getMethod()) {
- case GET -> get(request);
- case POST, PUT, DELETE, PATCH -> proxy(request);
- default -> ErrorResponse.methodNotAllowed("Method '" + request.getMethod() + "' is unsupported");
- };
- } catch (IllegalArgumentException e) {
- return ErrorResponse.badRequest(Exceptions.toMessageString(e));
- } catch (RuntimeException e) {
- return ErrorResponses.logThrowing(request, log, e);
- }
- }
-
- private HttpResponse get(HttpRequest request) {
- Path path = new Path(request.getUri());
- if (path.matches("/zone/v2")) {
- return root(request);
- }
- return proxy(request);
- }
-
- private HttpResponse proxy(HttpRequest request) {
- Path path = new Path(request.getUri());
- if ( ! path.matches("/zone/v2/{environment}/{region}/{*}")) {
- return notFound(path);
- }
- ZoneId zoneId = ZoneId.from(path.get("environment"), path.get("region"));
- if ( ! zoneRegistry.hasZone(zoneId)) {
- throw new IllegalArgumentException("No such zone: " + zoneId.value());
- }
- return proxy.handle(proxyRequest(zoneId, path.getRest(), request));
- }
-
- private HttpResponse root(HttpRequest request) {
- Slime slime = new Slime();
- Cursor root = slime.setObject();
- Cursor uris = root.setArray("uris");
- ZoneList zoneList = zoneRegistry.zones().reachable();
- zoneList.zones().forEach(zone -> uris.addString(request.getUri()
- .resolve("/zone/v2/")
- .resolve(zone.getEnvironment().value() + "/")
- .resolve(zone.getRegionName().value())
- .toString()));
- Cursor zones = root.setArray("zones");
- zoneList.zones().forEach(zone -> {
- Cursor object = zones.addObject();
- object.setString("environment", zone.getEnvironment().value());
- object.setString("region", zone.getRegionName().value());
- });
- return new SlimeJsonResponse(slime);
- }
-
- private HttpResponse notFound(Path path) {
- return ErrorResponse.notFoundError("Nothing at " + path);
- }
-
- private ProxyRequest proxyRequest(ZoneId zoneId, HttpURL.Path path, HttpRequest request) {
- return ProxyRequest.tryOne(zoneRegistry.getConfigServerVipUri(zoneId), path, request);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java
deleted file mode 100644
index 7902c38982c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author mpolden
- */
-package com.yahoo.vespa.hosted.controller.restapi.zone.v2;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/EndpointConfig.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/EndpointConfig.java
deleted file mode 100644
index 1d5bf5e6aa2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/EndpointConfig.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-/**
- * Endpoint configurations supported for an application.
- *
- * @author mpolden
- */
-public enum EndpointConfig {
-
- /** Only legacy endpoints will be published in DNS. Certificate will contain both legacy and generated names, and is never assigned from a pool */
- legacy,
-
- /** Legacy and generated endpoints will be published in DNS. Certificate will contain both legacy and generated names, and is never assigned from a pool */
- combined,
-
- /** Only generated endpoints will be published in DNS. Certificate will contain generated names only. Certificate is assigned from a pool */
- generated;
-
- /** Returns whether this config supports legacy endpoints */
- public boolean supportsLegacy() {
- return this == legacy || this == combined;
- }
-
- /** Returns whether this config supports generated endpoints */
- public boolean supportsGenerated() {
- return this == combined || this == generated;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/GeneratedEndpointList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/GeneratedEndpointList.java
deleted file mode 100644
index af1abff142b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/GeneratedEndpointList.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-import com.yahoo.collections.AbstractFilteringList;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.GeneratedEndpoint;
-
-import java.util.Collection;
-import java.util.List;
-
-/**
- * An immutable, filterable list of {@link GeneratedEndpoint}.
- *
- * @author mpolden
- */
-public class GeneratedEndpointList extends AbstractFilteringList<GeneratedEndpoint, GeneratedEndpointList> {
-
- public static final GeneratedEndpointList EMPTY = new GeneratedEndpointList(List.of(), false);
-
- private GeneratedEndpointList(Collection<? extends GeneratedEndpoint> items, boolean negate) {
- super(items, negate, GeneratedEndpointList::new);
- }
-
- /** Returns the subset of endpoints which are generated for given endpoint ID */
- public GeneratedEndpointList declared(EndpointId endpoint) {
- return matching(e -> e.endpoint().isPresent() && e.endpoint().get().equals(endpoint));
- }
-
- /** Returns the subset of endpoints which are generated for endpoints declared in {@link com.yahoo.config.application.api.DeploymentSpec} */
- public GeneratedEndpointList declared() {
- return matching(GeneratedEndpoint::declared);
- }
-
- /** Returns the subset endpoints which are generated for clusters declared in {@link com.yahoo.vespa.hosted.controller.application.pkg.BasicServicesXml} */
- public GeneratedEndpointList cluster() {
- return not().declared();
- }
-
- /** Returns the subset of endpoints matching given auth method */
- public GeneratedEndpointList authMethod(AuthMethod authMethod) {
- return matching(ge -> ge.authMethod() == authMethod);
- }
-
- public static GeneratedEndpointList of(GeneratedEndpoint... generatedEndpoint) {
- return copyOf(List.of(generatedEndpoint));
- }
-
- public static GeneratedEndpointList copyOf(Collection<GeneratedEndpoint> generatedEndpoints) {
- return generatedEndpoints.isEmpty() ? EMPTY : new GeneratedEndpointList(generatedEndpoints, false);
- }
-
- @Override
- public String toString() {
- return asList().toString();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/PreparedEndpoints.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/PreparedEndpoints.java
deleted file mode 100644
index 63b17a087f2..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/PreparedEndpoints.java
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerEndpoint;
-import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.EndpointList;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-/**
- * This represents the endpoints, and associated resources, that have been prepared for a deployment.
- *
- * @author mpolden
- */
-public record PreparedEndpoints(DeploymentId deployment,
- EndpointList endpoints,
- List<AssignedRotation> rotations,
- EndpointCertificate certificate) {
-
- public PreparedEndpoints(DeploymentId deployment, EndpointList endpoints, List<AssignedRotation> rotations, EndpointCertificate certificate) {
- this.deployment = Objects.requireNonNull(deployment);
- this.endpoints = Objects.requireNonNull(endpoints);
- this.rotations = List.copyOf(Objects.requireNonNull(rotations));
- this.certificate = requireMatchingSans(certificate, endpoints);
- }
-
- /** Returns the endpoints contained in this as {@link com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerEndpoint} */
- public Set<ContainerEndpoint> containerEndpoints() {
- Map<EndpointId, AssignedRotation> rotationsByEndpointId = rotations.stream()
- .collect(Collectors.toMap(AssignedRotation::endpointId,
- Function.identity()));
- Set<ContainerEndpoint> containerEndpoints = new HashSet<>();
- endpoints.scope(Endpoint.Scope.zone).groupingBy(Endpoint::cluster).forEach((clusterId, clusterEndpoints) -> {
- clusterEndpoints.groupingBy(Endpoint::authMethod).forEach((authMethod, endpointsByAuthMethod) -> {
- containerEndpoints.add(new ContainerEndpoint(clusterId.value(),
- asString(Endpoint.Scope.zone),
- endpointsByAuthMethod.mapToList(Endpoint::dnsName),
- OptionalInt.empty(),
- endpointsByAuthMethod.first().get().routingMethod(),
- authMethod));
- });
- });
- endpoints.scope(Endpoint.Scope.global).groupingBy(Endpoint::cluster).forEach((clusterId, clusterEndpoints) -> {
- for (var endpoint : clusterEndpoints) {
- List<String> names = new ArrayList<>(2);
- names.add(endpoint.dnsName());
- if (endpoint.requiresRotation()) {
- EndpointId endpointId = EndpointId.of(endpoint.name());
- AssignedRotation rotation = rotationsByEndpointId.get(endpointId);
- if (rotation == null) {
- throw new IllegalStateException(endpoint + " requires a rotation, but no rotation has been assigned to " + endpointId);
- }
- // Include the rotation ID as a valid name of this container endpoint
- // (required by global routing health checks)
- names.add(rotation.rotationId().asString());
- }
- containerEndpoints.add(new ContainerEndpoint(clusterId.value(),
- asString(Endpoint.Scope.global),
- names,
- OptionalInt.empty(),
- endpoint.routingMethod(),
- endpoint.authMethod()));
- }
- });
- endpoints.scope(Endpoint.Scope.application).groupingBy(Endpoint::cluster).forEach((clusterId, clusterEndpoints) -> {
- for (var endpoint : clusterEndpoints) {
- Optional<Endpoint.Target> matchingTarget = endpoint.targets().stream()
- .filter(t -> t.routesTo(deployment))
- .findFirst();
- if (matchingTarget.isEmpty()) throw new IllegalStateException("No target found routing to " + deployment + " in " + endpoint);
- containerEndpoints.add(new ContainerEndpoint(clusterId.value(),
- asString(Endpoint.Scope.application),
- List.of(endpoint.dnsName()),
- OptionalInt.of(matchingTarget.get().weight()),
- endpoint.routingMethod(),
- endpoint.authMethod()));
- }
- });
- return containerEndpoints;
- }
-
- private static String asString(Endpoint.Scope scope) {
- return switch (scope) {
- case application -> "application";
- case global -> "global";
- case weighted -> "weighted";
- case zone -> "zone";
- };
- }
-
- private static EndpointCertificate requireMatchingSans(EndpointCertificate certificate, EndpointList endpoints) {
- Objects.requireNonNull(certificate);
- for (var endpoint : endpoints.not().scope(Endpoint.Scope.weighted)) { // Weighted endpoints are not present in certificate
- if (!certificate.sanMatches(endpoint.dnsName())) {
- throw new IllegalArgumentException(endpoint + " has no matching SAN. Certificate contains " +
- certificate.requestedDnsSans());
- }
- }
- return certificate;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingId.java
deleted file mode 100644
index 50e54423f9a..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingId.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-
-import java.util.Objects;
-
-/**
- * Unique identifier for a instance routing table entry (instance x endpoint ID).
- *
- * @author mpolden
- */
-public record RoutingId(ApplicationId instance,
- EndpointId endpointId,
- TenantAndApplicationId application) {
-
- public RoutingId {
- Objects.requireNonNull(instance, "application must be non-null");
- Objects.requireNonNull(endpointId, "endpointId must be non-null");
- Objects.requireNonNull(application, "application must be non-null");
- }
-
- @Override
- public String toString() {
- return "routing id for " + endpointId + " of " + instance;
- }
-
- public static RoutingId of(ApplicationId instance, EndpointId endpoint) {
- return new RoutingId(instance, endpoint, TenantAndApplicationId.from(instance));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
deleted file mode 100644
index e93bc637a6b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java
+++ /dev/null
@@ -1,781 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-import ai.vespa.http.DomainName;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.LatencyAliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService.ChallengeState;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService.DnsChallenge;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedAliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedDirectTarget;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.EndpointList;
-import com.yahoo.vespa.hosted.controller.application.GeneratedEndpoint;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceForwarder;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue.Priority;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceRequest;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * Updates routing policies and their associated DNS records based on a deployment's load balancers.
- *
- * @author mortent
- * @author mpolden
- */
-public class RoutingPolicies {
-
- private static final Logger LOG = Logger.getLogger(RoutingPolicies.class.getName());
-
- private final Controller controller;
- private final CuratorDb db;
-
- public RoutingPolicies(Controller controller) {
- this.controller = Objects.requireNonNull(controller, "controller must be non-null");
- this.db = controller.curator();
- try (var lock = db.lockRoutingPolicies()) { // Update serialized format
- for (var policy : db.readRoutingPolicies().entrySet()) {
- db.writeRoutingPolicies(policy.getKey(), policy.getValue());
- }
- }
- }
-
- /** Read all routing policies for given deployment */
- public RoutingPolicyList read(DeploymentId deployment) {
- return read(deployment.applicationId()).deployment(deployment);
- }
-
- /** Read all routing policies for given instance */
- public RoutingPolicyList read(ApplicationId instance) {
- return RoutingPolicyList.copyOf(db.readRoutingPolicies(instance));
- }
-
- /** Read all routing policies for given application */
- public RoutingPolicyList read(TenantAndApplicationId application) {
- return db.readRoutingPolicies((instance) -> TenantAndApplicationId.from(instance).equals(application))
- .values()
- .stream()
- .flatMap(Collection::stream)
- .collect(Collectors.collectingAndThen(Collectors.toList(), RoutingPolicyList::copyOf));
- }
-
- /** Read all routing policies */
- private RoutingPolicyList readAll() {
- return db.readRoutingPolicies()
- .values()
- .stream()
- .flatMap(Collection::stream)
- .collect(Collectors.collectingAndThen(Collectors.toList(), RoutingPolicyList::copyOf));
- }
-
- /** Read routing policy for given zone */
- public ZoneRoutingPolicy read(ZoneId zone) {
- return db.readZoneRoutingPolicy(zone);
- }
-
- /**
- * Refresh routing policies for instance in given zone. This is idempotent and changes will only be performed if
- * routing configuration affecting given deployment has changed.
- */
- public void refresh(DeploymentId deployment, DeploymentSpec deploymentSpec, EndpointList generatedEndpoints) {
- if (!generatedEndpoints.not().generated().isEmpty()) {
- throw new IllegalStateException("Generated endpoints contains non-generated, got " + generatedEndpoints);
- }
- ApplicationId instance = deployment.applicationId();
- List<LoadBalancer> loadBalancers = controller.serviceRegistry().configServer()
- .getLoadBalancers(instance, deployment.zoneId());
- LoadBalancerAllocation allocation = new LoadBalancerAllocation(deployment, deploymentSpec, loadBalancers);
- Optional<TenantAndApplicationId> owner = ownerOf(allocation);
- try (var lock = db.lockRoutingPolicies()) {
- RoutingPolicyList applicationPolicies = read(TenantAndApplicationId.from(instance));
- RoutingPolicyList deploymentPolicies = applicationPolicies.deployment(allocation.deployment);
-
- removeGlobalDnsUnreferencedBy(allocation, deploymentPolicies, lock);
- removeApplicationDnsUnreferencedBy(allocation, deploymentPolicies, lock);
-
- RoutingPolicyList instancePolicies = storePoliciesOf(allocation, applicationPolicies, generatedEndpoints, lock);
- instancePolicies = removePoliciesUnreferencedBy(allocation, instancePolicies, lock);
-
- RoutingPolicyList updatedApplicationPolicies = applicationPolicies.replace(instance, instancePolicies);
- updateGlobalDnsOf(instancePolicies, Optional.of(deployment), owner, lock);
- updateApplicationDnsOf(updatedApplicationPolicies, deployment, owner, lock);
- }
- }
-
- /** Set the status of all global endpoints in given zone */
- public void setRoutingStatus(ZoneId zone, RoutingStatus.Value value) {
- try (var lock = db.lockRoutingPolicies()) {
- db.writeZoneRoutingPolicy(new ZoneRoutingPolicy(zone, RoutingStatus.create(value, RoutingStatus.Agent.operator,
- controller.clock().instant())));
- Map<ApplicationId, RoutingPolicyList> allPolicies = readAll().groupingBy(policy -> policy.id().owner());
- allPolicies.forEach((instance, policies) -> {
- updateGlobalDnsOf(policies, Optional.empty(), Optional.of(TenantAndApplicationId.from(instance)), lock);
- });
- }
- }
-
- /** Set the status of all global endpoints for given deployment */
- public void setRoutingStatus(DeploymentId deployment, RoutingStatus.Value value, RoutingStatus.Agent agent) {
- ApplicationId instance = deployment.applicationId();
- try (var lock = db.lockRoutingPolicies()) {
- RoutingPolicyList applicationPolicies = read(TenantAndApplicationId.from(instance));
- RoutingPolicyList deploymentPolicies = applicationPolicies.deployment(deployment);
- Map<RoutingPolicyId, RoutingPolicy> updatedPolicies = new LinkedHashMap<>(applicationPolicies.asMap());
- for (var policy : deploymentPolicies) {
- var newPolicy = policy.with(RoutingStatus.create(value, agent, controller.clock().instant()));
- updatedPolicies.put(policy.id(), newPolicy);
- }
- RoutingPolicyList effectivePolicies = RoutingPolicyList.copyOf(updatedPolicies.values());
- Map<ApplicationId, RoutingPolicyList> policiesByInstance = effectivePolicies.groupingBy(policy -> policy.id().owner());
- policiesByInstance.forEach((ignored, instancePolicies) -> updateGlobalDnsOf(instancePolicies,
- Optional.of(deployment),
- ownerOf(deployment),
- lock));
- updateApplicationDnsOf(effectivePolicies, deployment, ownerOf(deployment), lock);
- policiesByInstance.forEach((owner, instancePolicies) -> db.writeRoutingPolicies(owner, instancePolicies.asList()));
- }
- }
-
- /** Update global DNS records for given policies */
- private void updateGlobalDnsOf(RoutingPolicyList instancePolicies, Optional<DeploymentId> deployment,
- Optional<TenantAndApplicationId> owner,
- @SuppressWarnings("unused") Mutex lock) {
- Map<RoutingId, List<RoutingPolicy>> routingTable = instancePolicies.asInstanceRoutingTable();
- for (Map.Entry<RoutingId, List<RoutingPolicy>> routeEntry : routingTable.entrySet()) {
- RoutingId routingId = routeEntry.getKey();
- controller.routing().readDeclaredEndpointsOf(routingId.instance())
- .named(routingId.endpointId(), Endpoint.Scope.global)
- .not().requiresRotation()
- .forEach(endpoint -> updateGlobalDnsOf(endpoint, routeEntry.getValue(), deployment, owner));
- }
- }
-
- /** Update global DNS records for given global endpoint */
- private void updateGlobalDnsOf(Endpoint endpoint, List<RoutingPolicy> policies,
- Optional<DeploymentId> deployment, Optional<TenantAndApplicationId> owner) {
- if (endpoint.scope() != Endpoint.Scope.global) throw new IllegalStateException("Endpoint " + endpoint + " is not global");
- if (deployment.isPresent() && !endpoint.deployments().contains(deployment.get())) return;
-
- Collection<RegionEndpoint> regionEndpoints = computeRegionEndpoints(endpoint, policies);
- Set<AliasTarget> latencyTargets = new LinkedHashSet<>();
- Set<AliasTarget> inactiveLatencyTargets = new LinkedHashSet<>();
- for (var regionEndpoint : regionEndpoints) {
- if (regionEndpoint.active()) {
- latencyTargets.add(regionEndpoint.target());
- } else {
- inactiveLatencyTargets.add(regionEndpoint.target());
- }
- }
-
- // Refuse removal of last target in an endpoint. We do this because removing 100% of the ALIAS records would
- // cause the application endpoint to stop resolving entirely (NXDOMAIN).
- if (latencyTargets.isEmpty() && !inactiveLatencyTargets.isEmpty()) {
- if (deployment.isPresent()) {
- throw new IllegalArgumentException("Cannot deactivate routing for " + deployment.get() +
- " as it's the last remaining active deployment in " + endpoint);
- } else {
- // Operator is deactivating routing for entire zone, but this endpoint only has one target
- LOG.log(Level.WARNING, "Cannot deactivate routing for " + endpoint + " because it has only one " +
- "active zone. Leaving it in");
- return;
- }
- }
-
- // Create a weighted ALIAS per region, pointing to all zones within the same region
- regionEndpoints.forEach(regionEndpoint -> {
- if ( ! regionEndpoint.zoneAliasTargets().isEmpty()) {
- controller.nameServiceForwarder().createAlias(RecordName.from(regionEndpoint.target().name().value()),
- regionEndpoint.zoneAliasTargets(),
- Priority.normal,
- owner);
- }
- if ( ! regionEndpoint.zoneDirectTargets().isEmpty()) {
- controller.nameServiceForwarder().createDirect(RecordName.from(regionEndpoint.target().name().value()),
- regionEndpoint.zoneDirectTargets(),
- Priority.normal,
- owner);
- }
- });
-
- // Create global latency-based ALIAS pointing to each per-region weighted ALIAS
- controller.nameServiceForwarder().createAlias(RecordName.from(endpoint.dnsName()), latencyTargets, Priority.normal, owner);
- inactiveLatencyTargets.forEach(t -> controller.nameServiceForwarder()
- .removeRecords(Record.Type.ALIAS,
- RecordName.from(endpoint.dnsName()),
- RecordData.from(t.name().value()),
- Priority.normal,
- owner));
- }
-
- /** Compute region endpoints and their targets from given policies */
- private Collection<RegionEndpoint> computeRegionEndpoints(Endpoint parent, List<RoutingPolicy> policies) {
- if (!parent.scope().multiDeployment()) {
- throw new IllegalStateException(parent + " has unexpected scope, got " + parent.scope());
- }
- Map<Endpoint, RegionEndpoint> endpoints = new LinkedHashMap<>();
- for (var policy : policies) {
- if (policy.dnsZone().isEmpty() && policy.canonicalName().isPresent()) continue;
- if (controller.zoneRegistry().routingMethod(policy.id().zone()) != RoutingMethod.exclusive) continue;
- var zonePolicy = db.readZoneRoutingPolicy(policy.id().zone());
- // A record with 0 weight will not receive traffic. If all records within a group have 0
- // weight, traffic is routed to all records with equal probability
- long weight = isConfiguredOut(zonePolicy, policy) ? 0 : 1;
- boolean generated = parent.generated().isPresent();
- EndpointList weightedEndpoints = controller.routing()
- .endpointsOf(policy.id().deployment(),
- policy.id().cluster(),
- policy.generatedEndpoints().cluster())
- .scope(Endpoint.Scope.weighted);
- if (generated) {
- weightedEndpoints = weightedEndpoints.generated();
- } else {
- weightedEndpoints = weightedEndpoints.not().generated();
- }
- if (generated && weightedEndpoints.isEmpty()) {
- // Ignore this policy. If an instance has a global endpoint, and is switching from non-generated to
- // generated endpoints we cannot update global DNS record for a deployment until it has been deployed at
- // least once (which assigns a generated endpoint).
- continue;
- }
- if (weightedEndpoints.size() != 1) {
- throw new IllegalStateException("Expected to compute exactly one region endpoint for " + policy.id() + " with parent " + parent + ", got " + weightedEndpoints);
- }
- Endpoint endpoint = weightedEndpoints.first().get();
- RegionEndpoint regionEndpoint = endpoints.computeIfAbsent(endpoint, (k) -> new RegionEndpoint(
- new LatencyAliasTarget(DomainName.of(endpoint.dnsName()), policy.dnsZone().get(), policy.id().zone())));
-
- if (policy.canonicalName().isPresent()) {
- var weightedTarget = new WeightedAliasTarget(
- policy.canonicalName().get(), policy.dnsZone().get(), policy.id().zone().value(), weight);
- regionEndpoint.add(weightedTarget);
- } else {
- var weightedTarget = new WeightedDirectTarget(
- RecordData.from(policy.ipAddress().get()), policy.id().zone(), weight);
- regionEndpoint.add(weightedTarget);
- }
- }
- return endpoints.values();
- }
-
-
- private void updateApplicationDnsOf(RoutingPolicyList routingPolicies, DeploymentId deployment,
- Optional<TenantAndApplicationId> owner, @SuppressWarnings("unused") Mutex lock) {
- // In the context of single deployment (which this is) there is only one routing policy per routing ID. I.e.
- // there is no scenario where more than one deployment within an instance can be a member the same
- // application-level endpoint. However, to allow this in the future the routing table remains
- // Map<RoutingId, List<RoutingPolicy>> instead of Map<RoutingId, RoutingPolicy>.
- Map<RoutingId, List<RoutingPolicy>> routingTable = routingPolicies.asApplicationRoutingTable();
- if (routingTable.isEmpty()) return;
-
- Application application = controller.applications().requireApplication(routingTable.keySet().iterator().next().application());
- Map<Endpoint, Set<Target>> targetsByEndpoint = new LinkedHashMap<>();
- Map<Endpoint, Set<Target>> inactiveTargetsByEndpoint = new LinkedHashMap<>();
- for (Map.Entry<RoutingId, List<RoutingPolicy>> routeEntry : routingTable.entrySet()) {
- RoutingId routingId = routeEntry.getKey();
- EndpointList endpoints = controller.routing().readDeclaredEndpointsOf(application)
- .named(routingId.endpointId(), Endpoint.Scope.application);
- for (Endpoint endpoint : endpoints) {
- for (var policy : routeEntry.getValue()) {
- for (var target : endpoint.targets()) {
- if (!policy.appliesTo(target.deployment())) continue;
- if (policy.dnsZone().isEmpty() && policy.canonicalName().isPresent())
- continue; // Does not support ALIAS records
- ZoneRoutingPolicy zonePolicy = db.readZoneRoutingPolicy(policy.id().zone());
-
- Set<Target> activeTargets = targetsByEndpoint.computeIfAbsent(endpoint, (k) -> new LinkedHashSet<>());
- Set<Target> inactiveTargets = inactiveTargetsByEndpoint.computeIfAbsent(endpoint, (k) -> new LinkedHashSet<>());
- if (isConfiguredOut(zonePolicy, policy)) {
- inactiveTargets.add(Target.weighted(policy, target));
- } else {
- activeTargets.add(Target.weighted(policy, target));
- }
- }
- }
- }
- }
-
- // Refuse removal of last target in an endpoint. We do this because removing 100% of the ALIAS records would
- // cause the application endpoint to stop resolving entirely (NXDOMAIN).
- targetsByEndpoint.forEach((endpoint, targets) -> {
- if (targets.isEmpty()) {
- throw new IllegalArgumentException("Cannot deactivate routing for " + deployment +
- " as it's the last remaining active deployment in " + endpoint);
- }
- });
-
- // Create DNS records for active targets
- targetsByEndpoint.forEach((applicationEndpoint, targets) -> {
- // Where multiple zones are permitted, they all have the same routing policy, and nameServiceForwarder (below).
- ZoneId targetZone = applicationEndpoint.targets().iterator().next().deployment().zoneId();
- Set<AliasTarget> aliasTargets = new LinkedHashSet<>();
- Set<DirectTarget> directTargets = new LinkedHashSet<>();
- for (Target target : targets) {
- if (!target.deployment().equals(deployment)) continue; // Do not update target not matching this deployment
- if (target.aliasOrDirectTarget() instanceof AliasTarget at) {
- aliasTargets.add(at);
- } else {
- directTargets.add((DirectTarget) target.aliasOrDirectTarget());
- }
- }
- if (!aliasTargets.isEmpty()) {
- nameServiceForwarder(applicationEndpoint).createAlias(
- RecordName.from(applicationEndpoint.dnsName()), aliasTargets, Priority.normal, owner);
- }
- if (!directTargets.isEmpty()) {
- nameServiceForwarder(applicationEndpoint).createDirect(
- RecordName.from(applicationEndpoint.dnsName()), directTargets, Priority.normal, owner);
- }
- });
-
- // Remove DNS records for inactive targets
- inactiveTargetsByEndpoint.forEach((applicationEndpoint, targets) -> {
- targets.forEach(target -> {
- if (!target.deployment().equals(deployment)) return; // Do not update target not matching this deployment
- nameServiceForwarder(applicationEndpoint).removeRecords(target.type(),
- RecordName.from(applicationEndpoint.dnsName()),
- target.data(),
- Priority.normal,
- owner);
- });
- });
- }
-
- /**
- * Store routing policies for given load balancers
- *
- * @return the updated policies
- */
- private RoutingPolicyList storePoliciesOf(LoadBalancerAllocation allocation, RoutingPolicyList applicationPolicies, EndpointList generatedEndpoints, @SuppressWarnings("unused") Mutex lock) {
- Map<RoutingPolicyId, RoutingPolicy> policies = new LinkedHashMap<>(applicationPolicies.instance(allocation.deployment.applicationId()).asMap());
- for (LoadBalancer loadBalancer : allocation.loadBalancers) {
- if (loadBalancer.hostname().isEmpty() && loadBalancer.ipAddress().isEmpty()) continue;
- RoutingPolicyId policyId = new RoutingPolicyId(loadBalancer.application(), loadBalancer.cluster(), allocation.deployment.zoneId());
- RoutingPolicy existingPolicy = policies.get(policyId);
- Optional<String> dnsZone = loadBalancer.ipAddress().isPresent() ? Optional.of("ignored") : loadBalancer.dnsZone();
- List<GeneratedEndpoint> clusterGeneratedEndpoints = generatedEndpoints.cluster(loadBalancer.cluster())
- .mapToList(e -> e.generated().get());
- clusterGeneratedEndpoints.forEach(ge -> requireNonClashing(ge, applicationPolicies.without(existingPolicy)));
- var newPolicy = new RoutingPolicy(policyId, loadBalancer.hostname(), loadBalancer.ipAddress(), dnsZone,
- allocation.instanceEndpointsOf(loadBalancer),
- allocation.applicationEndpointsOf(loadBalancer),
- RoutingStatus.DEFAULT,
- loadBalancer.isPublic(),
- GeneratedEndpointList.copyOf(clusterGeneratedEndpoints));
- if (existingPolicy != null) {
- newPolicy = newPolicy.with(existingPolicy.routingStatus()); // Always preserve routing status
- }
- updateZoneDnsOf(newPolicy, loadBalancer, allocation.deployment);
- policies.put(newPolicy.id(), newPolicy);
- }
- RoutingPolicyList updated = RoutingPolicyList.copyOf(policies.values());
- db.writeRoutingPolicies(allocation.deployment.applicationId(), updated.asList());
- return updated;
- }
-
- /** Update zone DNS record for given policy */
- private void updateZoneDnsOf(RoutingPolicy policy, LoadBalancer loadBalancer, DeploymentId deploymentId) {
- EndpointList zoneEndpoints = controller.routing().endpointsOf(deploymentId,
- policy.id().cluster(),
- policy.generatedEndpoints().cluster())
- .scope(Endpoint.Scope.zone);
- for (var endpoint : zoneEndpoints) {
- RecordName name = RecordName.from(endpoint.dnsName());
- Record record = policy.canonicalName().isPresent() ?
- new Record(Record.Type.CNAME, name, RecordData.fqdn(policy.canonicalName().get().value())) :
- new Record(Record.Type.A, name, RecordData.from(policy.ipAddress().orElseThrow()));
- nameServiceForwarder(endpoint).createRecord(record, Priority.normal, ownerOf(deploymentId));
- }
- setPrivateDns(zoneEndpoints, loadBalancer, deploymentId);
- }
-
- private void setPrivateDns(EndpointList endpoints, LoadBalancer loadBalancer, DeploymentId deploymentId) {
- if (loadBalancer.service().isEmpty()) return;
- // TODO(mpolden): Model one service for each endpoint (type), to allow private endpoints with tokens.
- EndpointList mtlsEndpoints = endpoints.authMethod(AuthMethod.mtls);
- if (mtlsEndpoints.isEmpty()) return;
- Endpoint endpoint = mtlsEndpoints.generated().first().orElse(mtlsEndpoints.first().get());
- if (endpoint.routingMethod() != RoutingMethod.exclusive) return; // Not supported for this routing method
- controller.serviceRegistry().vpcEndpointService()
- .setPrivateDns(DomainName.of(endpoint.dnsName()),
- new ClusterId(deploymentId, endpoint.cluster()),
- loadBalancer.cloudAccount(),
- endpoint.generated().isPresent())
- .ifPresent(challenge -> {
- try (Mutex lock = db.lockNameServiceQueue()) {
- controller.nameServiceForwarder().createTxt(challenge.name(), List.of(challenge.data()), Priority.high, ownerOf(deploymentId));
- db.writeDnsChallenge(challenge);
- }
- });
- }
-
- /** Deletes all DNS challenges, and corresponding TXT records, for the given deployment. */
- public void removeDnsChallenges(DeploymentId deploymentId) {
- try (Mutex lock = db.lockNameServiceQueue()) {
- db.readDnsChallenges(deploymentId).forEach(this::removeDnsChallenge);
- }
- }
-
- /** Returns true iff. the given deployment has no incomplete DNS challenges, or throws (and cleans up) on errors. */
- public boolean processDnsChallenges(DeploymentId deploymentId) {
- try (Mutex lock = db.lockNameServiceQueue()) {
- List<DnsChallenge> challenges = new ArrayList<>(db.readDnsChallenges(deploymentId));
- challenges.removeIf(challenge -> challenge.state() == ChallengeState.done);
- Set<RecordName> pendingRequests = controller.curator().readNameServiceQueue().requests().stream()
- .map(NameServiceRequest::name)
- .collect(Collectors.toSet());
- try {
- challenges.removeIf(challenge -> {
- if (challenge.state() == ChallengeState.pending) {
- if (pendingRequests.contains(challenge.name())) return false;
- challenge = challenge.withState(ChallengeState.ready);
- }
- ChallengeState state = controller.serviceRegistry().vpcEndpointService().process(challenge);
- db.writeDnsChallenge(challenge.withState(state));
- return state == ChallengeState.done;
- });
- return challenges.isEmpty();
- }
- catch (RuntimeException e) {
- challenges.forEach(this::removeDnsChallenge);
- throw e;
- }
- }
- }
-
- private void removeDnsChallenge(DnsChallenge challenge) {
- controller.nameServiceForwarder().removeRecords(Record.Type.TXT, challenge.name(), Priority.normal, ownerOf(challenge.clusterId().deploymentId()));
- db.deleteDnsChallenge(challenge.clusterId());
- }
-
- /**
- * Remove policies and zone DNS records unreferenced by given load balancers
- *
- * @return the updated policies
- */
- private RoutingPolicyList removePoliciesUnreferencedBy(LoadBalancerAllocation allocation, RoutingPolicyList instancePolicies, @SuppressWarnings("unused") Mutex lock) {
- Map<RoutingPolicyId, RoutingPolicy> newPolicies = new LinkedHashMap<>(instancePolicies.asMap());
- Set<RoutingPolicyId> activeIds = allocation.asPolicyIds();
- RoutingPolicyList removable = instancePolicies.deployment(allocation.deployment)
- .not().matching(policy -> activeIds.contains(policy.id()));
- for (var policy : removable) {
- EndpointList zoneEndpoints = controller.routing().endpointsOf(allocation.deployment,
- policy.id().cluster(),
- policy.generatedEndpoints().cluster())
- .scope(Endpoint.Scope.zone);
- for (var endpoint : zoneEndpoints) {
- Record.Type type = policy.canonicalName().isPresent() ? Record.Type.CNAME : Record.Type.A;
- nameServiceForwarder(endpoint).removeRecords(type,
- RecordName.from(endpoint.dnsName()),
- Priority.normal,
- ownerOf(allocation));
- }
- newPolicies.remove(policy.id());
- }
- RoutingPolicyList updated = RoutingPolicyList.copyOf(newPolicies.values());
- db.writeRoutingPolicies(allocation.deployment.applicationId(), updated.asList());
- return updated;
- }
-
- /** Remove unreferenced instance endpoints from DNS */
- private void removeGlobalDnsUnreferencedBy(LoadBalancerAllocation allocation, RoutingPolicyList deploymentPolicies, @SuppressWarnings("unused") Mutex lock) {
- Map<RoutingId, List<RoutingPolicy>> routingTable = deploymentPolicies.asInstanceRoutingTable();
- Set<RoutingId> removalCandidates = new HashSet<>(routingTable.keySet());
- Set<RoutingId> activeRoutingIds = instanceRoutingIds(allocation);
- removalCandidates.removeAll(activeRoutingIds);
- for (var id : removalCandidates) {
- List<RoutingPolicy> policies = routingTable.get(id);
- Map<ClusterSpec.Id, List<RoutingPolicy>> policyByCluster = policies.stream().collect(Collectors.groupingBy(p -> p.id().cluster()));
- Set<Endpoint> endpoints = new LinkedHashSet<>();
- policyByCluster.forEach((cluster, clusterPolicies) -> {
- List<DeploymentId> deployments = clusterPolicies.stream().map(p -> p.id().deployment()).toList();
- GeneratedEndpointList generated = declaredGeneratedEndpoints(id.endpointId(), clusterPolicies);
- endpoints.addAll(controller.routing().declaredEndpointsOf(id, cluster, deployments, generated)
- .not().requiresRotation()
- .named(id.endpointId(), Endpoint.Scope.global).asList());
- });
- // This removes all ALIAS records having this DNS name. There is no attempt to delete only the entry for the
- // affected zone. Instead, the correct set of records is (re)created by updateGlobalDnsOf
- for (var endpoint : endpoints) {
- for (var regionEndpoint : computeRegionEndpoints(endpoint, deploymentPolicies.asList())) {
- Record.Type type = regionEndpoint.zoneDirectTargets().isEmpty() ? Record.Type.ALIAS : Record.Type.DIRECT;
- controller.nameServiceForwarder().removeRecords(type,
- RecordName.from(regionEndpoint.target().name().value()),
- Priority.normal,
- ownerOf(allocation));
- }
- nameServiceForwarder(endpoint).removeRecords(Record.Type.ALIAS, RecordName.from(endpoint.dnsName()),
- Priority.normal,
- ownerOf(allocation));
- }
- }
- }
-
- /** Remove unreferenced application endpoints in given allocation from DNS */
- private void removeApplicationDnsUnreferencedBy(LoadBalancerAllocation allocation, RoutingPolicyList deploymentPolicies, @SuppressWarnings("unused") Mutex lock) {
- Map<RoutingId, List<RoutingPolicy>> routingTable = deploymentPolicies.asApplicationRoutingTable();
- Set<RoutingId> removalCandidates = new HashSet<>(routingTable.keySet());
- Set<RoutingId> activeRoutingIds = applicationRoutingIds(allocation);
- removalCandidates.removeAll(activeRoutingIds);
- for (var id : removalCandidates) {
- TenantAndApplicationId application = TenantAndApplicationId.from(id.instance());
- List<RoutingPolicy> policies = routingTable.get(id);
- Map<ClusterSpec.Id, List<RoutingPolicy>> policyByCluster = policies.stream().collect(Collectors.groupingBy(p -> p.id().cluster()));
- Set<Endpoint> endpoints = new LinkedHashSet<>();
- policyByCluster.forEach((cluster, clusterPolicies) -> {
- // Weights are not available in this context, but they're not used for anything when removing records
- Map<DeploymentId, Integer> deployments = clusterPolicies.stream()
- .map(p -> p.id().deployment())
- .collect(Collectors.toMap(Function.identity(), (ignored) -> 1));
- GeneratedEndpointList generated = declaredGeneratedEndpoints(id.endpointId(), clusterPolicies);
- endpoints.addAll(controller.routing().declaredEndpointsOf(application, id.endpointId(), cluster,
- deployments, generated).asList());
- });
- for (var policy : policies) {
- if (!policy.appliesTo(allocation.deployment)) continue;
- for (Endpoint endpoint : endpoints) {
- NameServiceForwarder forwarder = nameServiceForwarder(endpoint);
- if (policy.canonicalName().isPresent()) {
- forwarder.removeRecords(Record.Type.ALIAS,
- RecordName.from(endpoint.dnsName()),
- RecordData.fqdn(policy.canonicalName().get().value()),
- Priority.normal,
- ownerOf(allocation));
- } else {
- forwarder.removeRecords(Record.Type.DIRECT,
- RecordName.from(endpoint.dnsName()),
- RecordData.from(policy.ipAddress().get()),
- Priority.normal,
- ownerOf(allocation));
- }
- }
- }
- }
- }
-
- private Set<RoutingId> instanceRoutingIds(LoadBalancerAllocation allocation) {
- return routingIdsFrom(allocation, false);
- }
-
- private Set<RoutingId> applicationRoutingIds(LoadBalancerAllocation allocation) {
- return routingIdsFrom(allocation, true);
- }
-
- private static GeneratedEndpointList declaredGeneratedEndpoints(EndpointId endpoint, List<RoutingPolicy> clusterPolicies) {
- return GeneratedEndpointList.copyOf(clusterPolicies.stream()
- .flatMap(p -> p.generatedEndpoints().declared(endpoint).asList().stream())
- .distinct()
- .toList());
- }
-
- /** Compute routing IDs from given load balancers */
- private static Set<RoutingId> routingIdsFrom(LoadBalancerAllocation allocation, boolean applicationLevel) {
- Set<RoutingId> routingIds = new LinkedHashSet<>();
- for (var loadBalancer : allocation.loadBalancers) {
- Set<EndpointId> endpoints = applicationLevel
- ? allocation.applicationEndpointsOf(loadBalancer)
- : allocation.instanceEndpointsOf(loadBalancer);
- for (var endpointId : endpoints) {
- routingIds.add(RoutingId.of(loadBalancer.application(), endpointId));
- }
- }
- return Collections.unmodifiableSet(routingIds);
- }
-
- /** Returns whether the endpoints of given policy are configured {@link RoutingStatus.Value#out} */
- private static boolean isConfiguredOut(ZoneRoutingPolicy zonePolicy, RoutingPolicy policy) {
- // A deployment can be configured out from endpoints at any of the following levels:
- // - zone level (ZoneRoutingPolicy)
- // - deployment level (RoutingPolicy)
- return zonePolicy.routingStatus().value() == RoutingStatus.Value.out ||
- policy.routingStatus().value() == RoutingStatus.Value.out;
- }
-
- /** Represents records for a region-wide endpoint */
- private static class RegionEndpoint {
-
- private final LatencyAliasTarget target;
- private final Set<WeightedAliasTarget> zoneAliasTargets = new LinkedHashSet<>();
- private final Set<WeightedDirectTarget> zoneDirectTargets = new LinkedHashSet<>();
-
- public RegionEndpoint(LatencyAliasTarget target) {
- this.target = Objects.requireNonNull(target);
- }
-
- public LatencyAliasTarget target() { return target; }
- public Set<AliasTarget> zoneAliasTargets() { return Collections.unmodifiableSet(zoneAliasTargets); }
- public Set<DirectTarget> zoneDirectTargets() { return Collections.unmodifiableSet(zoneDirectTargets); }
-
- public void add(WeightedAliasTarget target) { zoneAliasTargets.add(target); }
- public void add(WeightedDirectTarget target) { zoneDirectTargets.add(target); }
-
- public boolean active() {
- return zoneAliasTargets.stream().anyMatch(target -> target.weight() > 0) ||
- zoneDirectTargets.stream().anyMatch(target -> target.weight() > 0);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- RegionEndpoint that = (RegionEndpoint) o;
- return target.name().equals(that.target.name());
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(target.name());
- }
-
- }
-
- /** Active load balancers allocated to a deployment */
- record LoadBalancerAllocation(DeploymentId deployment,
- DeploymentSpec deploymentSpec,
- List<LoadBalancer> loadBalancers) {
-
- public LoadBalancerAllocation(DeploymentId deployment,
- DeploymentSpec deploymentSpec,
- List<LoadBalancer> loadBalancers) {
- this.deployment = deployment;
- this.loadBalancers = loadBalancers.stream().filter(LoadBalancerAllocation::isActive).toList();
- this.deploymentSpec = deploymentSpec;
- }
-
- private static boolean isActive(LoadBalancer loadBalancer) {
- return switch (loadBalancer.state()) {
- // Count reserved as active as we want to do DNS updates as early as possible
- case reserved, active -> true;
- default -> false;
- };
- }
-
- /** Returns the policy IDs of the load balancers contained in this */
- private Set<RoutingPolicyId> asPolicyIds() {
- return loadBalancers.stream()
- .map(lb -> new RoutingPolicyId(lb.application(),
- lb.cluster(),
- deployment.zoneId()))
- .collect(Collectors.toUnmodifiableSet());
- }
-
- /** Returns all instance endpoint IDs served by given load balancer */
- private Set<EndpointId> instanceEndpointsOf(LoadBalancer loadBalancer) {
- if (!deployment.zoneId().environment().isProduction()) { // Only production deployments have configurable endpoints
- return Set.of();
- }
- var instanceSpec = deploymentSpec.instance(loadBalancer.application().instance());
- if (instanceSpec.isEmpty()) {
- return Set.of();
- }
- return instanceSpec.get().endpoints().stream()
- .filter(endpoint -> endpoint.containerId().equals(loadBalancer.cluster().value()))
- .filter(endpoint -> endpoint.regions().contains(deployment.zoneId().region()))
- .map(com.yahoo.config.application.api.Endpoint::endpointId)
- .map(EndpointId::of)
- .collect(Collectors.toUnmodifiableSet());
- }
-
- /** Returns all application endpoint IDs served by given load balancer */
- private Set<EndpointId> applicationEndpointsOf(LoadBalancer loadBalancer) {
- if (!deployment.zoneId().environment().isProduction()) { // Only production deployments have configurable endpoints
- return Set.of();
- }
- return deploymentSpec.endpoints().stream()
- .filter(endpoint -> endpoint.containerId().equals(loadBalancer.cluster().value()))
- .filter(endpoint -> endpoint.targets().stream()
- .anyMatch(target -> target.region().equals(deployment.zoneId().region()) &&
- target.instance().equals(deployment.applicationId().instance())))
- .map(com.yahoo.config.application.api.Endpoint::endpointId)
- .map(EndpointId::of)
- .collect(Collectors.toUnmodifiableSet());
- }
-
- }
-
- /** Returns the name updater to use for given endpoint */
- private NameServiceForwarder nameServiceForwarder(Endpoint endpoint) {
- return switch (endpoint.routingMethod()) {
- case exclusive -> controller.nameServiceForwarder();
- case sharedLayer4 -> endpoint.generated().isPresent() ? controller.nameServiceForwarder() : new NameServiceDiscarder(controller.curator());
- };
- }
-
- /** Denotes record data (record rhs) of either an ALIAS or a DIRECT target */
- private record Target(Record.Type type, RecordData data, DeploymentId deployment, Object aliasOrDirectTarget) {
- static Target weighted(RoutingPolicy policy, Endpoint.Target endpointTarget) {
- if (policy.ipAddress().isPresent()) {
- var wt = new WeightedDirectTarget(RecordData.from(policy.ipAddress().get()),
- endpointTarget.deployment().zoneId(), endpointTarget.weight());
- return new Target(Record.Type.DIRECT, wt.recordData(), endpointTarget.deployment(), wt);
- }
- var wt = new WeightedAliasTarget(policy.canonicalName().get(), policy.dnsZone().get(),
- endpointTarget.deployment().zoneId().value(), endpointTarget.weight());
- return new Target(Record.Type.ALIAS, RecordData.fqdn(wt.name().value()), endpointTarget.deployment(), wt);
- }
- }
-
- /** A {@link NameServiceForwarder} that does nothing. Used in zones where no explicit DNS updates are needed */
- private static class NameServiceDiscarder extends NameServiceForwarder {
-
- public NameServiceDiscarder(CuratorDb db) {
- super(db);
- }
-
- @Override
- protected void forward(NameServiceRequest request, Priority priority) {
- // Ignored
- }
- }
-
- private static Optional<TenantAndApplicationId> ownerOf(DeploymentId deploymentId) {
- return Optional.of(TenantAndApplicationId.from(deploymentId.applicationId()));
- }
-
- private static Optional<TenantAndApplicationId> ownerOf(LoadBalancerAllocation allocation) {
- return ownerOf(allocation.deployment);
- }
-
- private static void requireNonClashing(GeneratedEndpoint generatedEndpoint, RoutingPolicyList applicationPolicies) {
- for (var policy : applicationPolicies) {
- for (var other : policy.generatedEndpoints()) {
- if (other.clusterPart().equals(generatedEndpoint.clusterPart()) && !other.endpoint().equals(generatedEndpoint.endpoint())) {
- throw new IllegalStateException(generatedEndpoint + " clashes with " + other + " in " + policy.id());
- }
- }
- }
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java
deleted file mode 100644
index fc72f3ed663..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-import ai.vespa.http.DomainName;
-import com.google.common.collect.ImmutableSortedSet;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * Represents the DNS routing policy for a {@link com.yahoo.vespa.hosted.controller.application.Deployment}.
- *
- * @author mortent
- * @author mpolden
- */
-public record RoutingPolicy(RoutingPolicyId id,
- Optional<DomainName> canonicalName,
- Optional<String> ipAddress,
- Optional<String> dnsZone,
- Set<EndpointId> instanceEndpoints,
- Set<EndpointId> applicationEndpoints,
- RoutingStatus routingStatus,
- boolean isPublic,
- GeneratedEndpointList generatedEndpoints) {
-
- /** DO NOT USE. Public for serialization purposes */
- public RoutingPolicy(RoutingPolicyId id, Optional<DomainName> canonicalName, Optional<String> ipAddress, Optional<String> dnsZone,
- Set<EndpointId> instanceEndpoints, Set<EndpointId> applicationEndpoints, RoutingStatus routingStatus, boolean isPublic,
- GeneratedEndpointList generatedEndpoints) {
- this.id = Objects.requireNonNull(id, "id must be non-null");
- this.canonicalName = Objects.requireNonNull(canonicalName, "canonicalName must be non-null");
- this.ipAddress = Objects.requireNonNull(ipAddress, "ipAddress must be non-null");
- this.dnsZone = Objects.requireNonNull(dnsZone, "dnsZone must be non-null");
- this.instanceEndpoints = ImmutableSortedSet.copyOf(Objects.requireNonNull(instanceEndpoints, "instanceEndpoints must be non-null"));
- this.applicationEndpoints = ImmutableSortedSet.copyOf(Objects.requireNonNull(applicationEndpoints, "applicationEndpoints must be non-null"));
- this.routingStatus = Objects.requireNonNull(routingStatus, "status must be non-null");
- this.isPublic = isPublic;
- this.generatedEndpoints = Objects.requireNonNull(generatedEndpoints, "generatedEndpoints must be non-null");
-
- if (canonicalName.isEmpty() == ipAddress.isEmpty())
- throw new IllegalArgumentException("Exactly 1 of canonicalName=%s and ipAddress=%s must be set".formatted(
- canonicalName.map(DomainName::value).orElse("<empty>"), ipAddress.orElse("<empty>")));
- if ( ! instanceEndpoints.isEmpty() && ! isPublic)
- throw new IllegalArgumentException("Non-public zone endpoint cannot be part of any global endpoint, but was in: " + instanceEndpoints);
- if ( ! applicationEndpoints.isEmpty() && ! isPublic)
- throw new IllegalArgumentException("Non-public zone endpoint cannot be part of any application endpoint, but was in: " + applicationEndpoints);
- }
-
- /** The ID of this */
- public RoutingPolicyId id() {
- return id;
- }
-
- /** The canonical name for the load balancer this applies to (rhs of a CNAME or ALIAS record) */
- public Optional<DomainName> canonicalName() {
- return canonicalName;
- }
-
- /** The IP address for the load balancer this applies to (rhs of an A or DIRECT record) */
- public Optional<String> ipAddress() {
- return ipAddress;
- }
-
- /** DNS zone for the load balancer this applies to, if any. Used when creating ALIAS records. */
- public Optional<String> dnsZone() {
- return dnsZone;
- }
-
- /** The instance-level endpoints this participates in */
- public Set<EndpointId> instanceEndpoints() {
- return instanceEndpoints;
- }
-
- /** The application-level endpoints this participates in */
- public Set<EndpointId> applicationEndpoints() {
- return applicationEndpoints;
- }
-
- /** The endpoints generated for this policy, if any */
- public GeneratedEndpointList generatedEndpoints() {
- return generatedEndpoints;
- }
-
- /** Return status of routing */
- public RoutingStatus routingStatus() {
- return routingStatus;
- }
-
- /** Returns whether this has a load balancer which is available from public internet. */
- public boolean isPublic() {
- return isPublic;
- }
-
- /** Returns whether this policy applies to given deployment */
- public boolean appliesTo(DeploymentId deployment) {
- return id.owner().equals(deployment.applicationId()) &&
- id.zone().equals(deployment.zoneId());
- }
-
- /** Returns a copy of this with routing status set to given status */
- public RoutingPolicy with(RoutingStatus routingStatus) {
- return new RoutingPolicy(id, canonicalName, ipAddress, dnsZone, instanceEndpoints, applicationEndpoints, routingStatus, isPublic, generatedEndpoints);
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- RoutingPolicy that = (RoutingPolicy) o;
- return id.equals(that.id);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicyId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicyId.java
deleted file mode 100644
index ea8ae6820c9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicyId.java
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-
-import java.util.Objects;
-
-/**
- * Unique identifier for a {@link RoutingPolicy}.
- *
- * @author mpolden
- */
-public record RoutingPolicyId(ApplicationId owner, ClusterSpec.Id cluster, ZoneId zone) {
-
- public RoutingPolicyId {
- Objects.requireNonNull(owner, "owner must be non-null");
- Objects.requireNonNull(cluster, "cluster must be non-null");
- Objects.requireNonNull(zone, "zone must be non-null");
- }
-
- /** The deployment this applies to */
- public DeploymentId deployment() {
- return new DeploymentId(owner, zone);
- }
-
- @Override
- public String toString() {
- return "routing policy for " + cluster + ", in " + zone + ", owned by " + owner;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicyList.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicyList.java
deleted file mode 100644
index f96275a0d5a..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicyList.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-import com.yahoo.collections.AbstractFilteringList;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-/**
- * A filterable list of {@link RoutingPolicy}'s.
- *
- * This is immutable.
- *
- * @author mpolden
- */
-public class RoutingPolicyList extends AbstractFilteringList<RoutingPolicy, RoutingPolicyList> {
-
- private final Map<RoutingPolicyId, RoutingPolicy> policiesById;
-
- protected RoutingPolicyList(Collection<RoutingPolicy> items, boolean negate) {
- super(items, negate, RoutingPolicyList::new);
- this.policiesById = items.stream().collect(Collectors.collectingAndThen(
- Collectors.toMap(RoutingPolicy::id,
- Function.identity(),
- (p1, p2) -> {
- throw new IllegalArgumentException("Duplicate key " + p1.id());
- },
- LinkedHashMap::new),
- Collections::unmodifiableMap)
- );
- }
-
- /** Returns the subset of policies owned by given instance */
- public RoutingPolicyList instance(ApplicationId instance) {
- return matching(policy -> policy.id().owner().equals(instance));
- }
-
- /** Returns the subset of policies applying to given cluster */
- public RoutingPolicyList cluster(ClusterSpec.Id cluster) {
- return matching(policy -> policy.id().cluster().equals(cluster));
- }
-
- /** Returns the subset of policies applying to given deployment */
- public RoutingPolicyList deployment(DeploymentId deployment) {
- return matching(policy -> policy.appliesTo(deployment));
- }
-
- /** Returns the policy with given ID, if any */
- public Optional<RoutingPolicy> of(RoutingPolicyId id) {
- return Optional.ofNullable(policiesById.get(id));
- }
-
- /** Returns this grouped by policy ID */
- public Map<RoutingPolicyId, RoutingPolicy> asMap() {
- return policiesById;
- }
-
- /** Returns a copy of this with all policies for instance replaced with given policies */
- public RoutingPolicyList replace(ApplicationId instance, RoutingPolicyList policies) {
- List<RoutingPolicy> copy = new ArrayList<>(asList());
- copy.removeIf(policy -> policy.id().owner().equals(instance));
- policies.forEach(copy::add);
- return copyOf(copy);
- }
-
- /** Returns a copy of this excluding the given policy */
- public RoutingPolicyList without(RoutingPolicy policy) {
- List<RoutingPolicy> copy = new ArrayList<>(asList());
- copy.remove(policy);
- return copyOf(copy);
- }
-
- /** Create a routing table for instance-level endpoints backed by routing policies in this */
- Map<RoutingId, List<RoutingPolicy>> asInstanceRoutingTable() {
- return asRoutingTable(false);
- }
-
- /** Create a routing table for application-level endpoints backed by routing policies in this */
- Map<RoutingId, List<RoutingPolicy>> asApplicationRoutingTable() {
- return asRoutingTable(true);
- }
-
- private Map<RoutingId, List<RoutingPolicy>> asRoutingTable(boolean applicationLevel) {
- Map<RoutingId, List<RoutingPolicy>> routingTable = new LinkedHashMap<>();
- for (var policy : this) {
- Set<EndpointId> endpoints = applicationLevel ? policy.applicationEndpoints() : policy.instanceEndpoints();
- for (var endpoint : endpoints) {
- RoutingId id = RoutingId.of(policy.id().owner(), endpoint);
- routingTable.computeIfAbsent(id, k -> new ArrayList<>())
- .add(policy);
- }
- }
- return Collections.unmodifiableMap(routingTable);
- }
-
- public static RoutingPolicyList copyOf(Collection<RoutingPolicy> policies) {
- return new RoutingPolicyList(policies, false);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingStatus.java
deleted file mode 100644
index bd46760cc3e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingStatus.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-import java.time.Instant;
-import java.util.Objects;
-
-/**
- * Represents the routing status of a {@link RoutingPolicy} or {@link ZoneRoutingPolicy}.
- *
- * This describes which agent last changed the routing status and at which time.
- *
- * This is immutable.
- *
- * @author mpolden
- */
-public record RoutingStatus(Value value, Agent agent, Instant changedAt) {
-
- public static final RoutingStatus DEFAULT = new RoutingStatus(Value.in, Agent.system, Instant.EPOCH);
-
- /** DO NOT USE. Public for serialization purposes */
- public RoutingStatus {
- Objects.requireNonNull(value, "value must be non-null");
- Objects.requireNonNull(agent, "agent must be non-null");
- Objects.requireNonNull(changedAt, "changedAt must be non-null");
- }
-
- /**
- * The wanted value of this. The system will try to set this value, but there are constraints that may lead to
- * the effective value not matching this. See {@link RoutingPolicies}.
- */
- public Value value() {
- return value;
- }
-
- /** The agent who last changed this */
- public Agent agent() {
- return agent;
- }
-
- /** The time this was last changed */
- public Instant changedAt() {
- return changedAt;
- }
-
- @Override
- public String toString() {
- return "status " + value + ", changed by " + agent + " @ " + changedAt;
- }
-
- public static RoutingStatus create(Value value, Agent agent, Instant instant) {
- return new RoutingStatus(value, agent, instant);
- }
-
- // Used in serialization. Do not change.
- public enum Value {
- /** Status is determined by health checks **/
- in,
-
- /** Status is explicitly set to out */
- out,
- }
-
- /** Agents that can change the state of global routing */
- public enum Agent {
- operator,
- tenant,
- system,
- unknown, // For compatibility old values from /routing/v1 on config server, which may contain a specific username.
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/ZoneRoutingPolicy.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/ZoneRoutingPolicy.java
deleted file mode 100644
index 3ca72a7dd67..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/ZoneRoutingPolicy.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.Objects;
-
-/**
- * Represents the DNS routing policy for a zone. This takes precedence over of a deployment-specific
- * {@link RoutingPolicy}.
- *
- * This is immutable.
- *
- * @author mpolden
- */
-public record ZoneRoutingPolicy(ZoneId zone, RoutingStatus routingStatus) {
-
- public ZoneRoutingPolicy {
- Objects.requireNonNull(zone, "zone must be non-null");
- Objects.requireNonNull(routingStatus, "globalRouting must be non-null");
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/DeploymentRoutingContext.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/DeploymentRoutingContext.java
deleted file mode 100644
index 50e65187835..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/DeploymentRoutingContext.java
+++ /dev/null
@@ -1,162 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.context;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.vespa.hosted.controller.LockedApplication;
-import com.yahoo.vespa.hosted.controller.RoutingController;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.EndpointList;
-import com.yahoo.vespa.hosted.controller.application.pkg.BasicServicesXml;
-import com.yahoo.vespa.hosted.controller.routing.PreparedEndpoints;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicy;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicyId;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-
-import java.time.Clock;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * A deployment routing context. This extends {@link RoutingContext} to support configuration of routing for a deployment.
- *
- * @author mpolden
- */
-public abstract class DeploymentRoutingContext implements RoutingContext {
-
- final DeploymentId deployment;
- final RoutingController routing;
- final RoutingMethod method;
-
- public DeploymentRoutingContext(DeploymentId deployment, RoutingMethod method, RoutingController routing) {
- this.deployment = Objects.requireNonNull(deployment);
- this.routing = Objects.requireNonNull(routing);
- this.method = Objects.requireNonNull(method);
- }
-
- /**
- * Prepare routing configuration for the deployment in this context
- *
- * @return the container endpoints relevant for this deployment, as declared in deployment spec
- */
- public final PreparedEndpoints prepare(BasicServicesXml services, EndpointCertificate certificate, LockedApplication application) {
- return routing.prepare(deployment, services, certificate, application);
- }
-
- /** Finalize routing configuration for the deployment in this context, using given deployment spec */
- public final void activate(DeploymentSpec deploymentSpec, EndpointList generatedEndpoints) {
- routing.policies().refresh(deployment, deploymentSpec, generatedEndpoints);
- }
-
- /** Deactivate routing configuration for the deployment in this context, using given deployment spec */
- public final void deactivate(DeploymentSpec deploymentSpec) {
- routing.policies().refresh(deployment, deploymentSpec, EndpointList.EMPTY);
- routing.policies().removeDnsChallenges(deployment);
- }
-
- /** Routing method of this context */
- public final RoutingMethod routingMethod() {
- return method;
- }
-
- /** Read the routing policy for given cluster in this deployment */
- public final Optional<RoutingPolicy> routingPolicy(ClusterSpec.Id cluster) {
- RoutingPolicyId id = new RoutingPolicyId(deployment.applicationId(), cluster, deployment.zoneId());
- return routing.policies().read(deployment).of(id);
- }
-
- /** Extension of a {@link DeploymentRoutingContext} for deployments using {@link RoutingMethod#sharedLayer4} routing */
- public static class SharedDeploymentRoutingContext extends DeploymentRoutingContext {
-
- private final Clock clock;
- private final ConfigServer configServer;
-
- public SharedDeploymentRoutingContext(DeploymentId deployment, RoutingController controller, ConfigServer configServer, Clock clock) {
- super(deployment, RoutingMethod.sharedLayer4, controller);
- this.clock = Objects.requireNonNull(clock);
- this.configServer = Objects.requireNonNull(configServer);
- }
-
- @Override
- public void setRoutingStatus(RoutingStatus.Value value, RoutingStatus.Agent agent) {
- EndpointStatus newStatus = new EndpointStatus(value == RoutingStatus.Value.in
- ? EndpointStatus.Status.in
- : EndpointStatus.Status.out,
- agent.name(),
- clock.instant());
- try {
- configServer.setGlobalRotationStatus(deployment, upstreamNames(), newStatus);
- } catch (Exception e) {
- throw new RuntimeException("Failed to change rotation status of " + deployment, e);
- }
- }
-
- @Override
- public RoutingStatus routingStatus() {
- // In a given deployment, all upstreams (clusters) share the same status, so we can query using any
- // upstream name
- String upstreamName = upstreamNames().get(0);
- EndpointStatus status = configServer.getGlobalRotationStatus(deployment, upstreamName);
- RoutingStatus.Agent agent;
- try {
- agent = RoutingStatus.Agent.valueOf(status.agent().toLowerCase());
- } catch (IllegalArgumentException e) {
- agent = RoutingStatus.Agent.unknown;
- }
- return new RoutingStatus(status.status() == EndpointStatus.Status.in
- ? RoutingStatus.Value.in
- : RoutingStatus.Value.out,
- agent,
- status.changedAt());
- }
-
- private List<String> upstreamNames() {
- List<String> upstreamNames = routing.readEndpointsOf(deployment)
- .scope(Endpoint.Scope.zone)
- .shared()
- .asList().stream()
- .map(endpoint -> endpoint.upstreamName(deployment))
- .distinct()
- .toList();
- if (upstreamNames.isEmpty()) {
- throw new IllegalArgumentException("No upstream names found for " + deployment);
- }
- return upstreamNames;
- }
-
- }
-
- /**
- * Implementation of a {@link DeploymentRoutingContext} for deployments using {@link RoutingMethod#exclusive}
- * routing.
- */
- public static class ExclusiveDeploymentRoutingContext extends DeploymentRoutingContext {
-
- public ExclusiveDeploymentRoutingContext(DeploymentId deployment, RoutingController controller) {
- super(deployment, RoutingMethod.exclusive, controller);
- }
-
- @Override
- public void setRoutingStatus(RoutingStatus.Value value, RoutingStatus.Agent agent) {
- routing.policies().setRoutingStatus(deployment, value, agent);
- }
-
- @Override
- public RoutingStatus routingStatus() {
- // Status for a deployment applies to all clusters within the deployment, so we use the status from the
- // first matching policy here
- return routing.policies().read(deployment)
- .first()
- .map(RoutingPolicy::routingStatus)
- .orElse(RoutingStatus.DEFAULT);
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/ExclusiveZoneRoutingContext.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/ExclusiveZoneRoutingContext.java
deleted file mode 100644
index 201baa78437..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/ExclusiveZoneRoutingContext.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.context;
-
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicies;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-
-import java.util.Objects;
-
-/**
- * An implementation of {@link RoutingContext} for a zone using {@link RoutingMethod#exclusive} routing.
- *
- * @author mpolden
- */
-public class ExclusiveZoneRoutingContext implements RoutingContext {
-
- private final RoutingPolicies policies;
- private final ZoneId zone;
-
- public ExclusiveZoneRoutingContext(ZoneId zone, RoutingPolicies policies) {
- this.policies = Objects.requireNonNull(policies);
- this.zone = Objects.requireNonNull(zone);
- }
-
- @Override
- public void setRoutingStatus(RoutingStatus.Value value, RoutingStatus.Agent agent) {
- policies.setRoutingStatus(zone, value);
- }
-
- @Override
- public RoutingStatus routingStatus() {
- return policies.read(zone).routingStatus();
- }
-
- @Override
- public RoutingMethod routingMethod() {
- return RoutingMethod.exclusive;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/RoutingContext.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/RoutingContext.java
deleted file mode 100644
index 84315e319ec..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/RoutingContext.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.context;
-
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-
-/**
- * Top-level interface for a routing context, which provides control of routing status for a deployment or zone.
- *
- * @author mpolden
- */
-public interface RoutingContext {
-
- /** Change the routing status for the zone or deployment represented by this context */
- void setRoutingStatus(RoutingStatus.Value value, RoutingStatus.Agent agent);
-
- /** Get the current routing status for the zone or deployment represented by this context */
- RoutingStatus routingStatus();
-
- /** Routing method used in this context */
- RoutingMethod routingMethod();
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/SharedZoneRoutingContext.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/SharedZoneRoutingContext.java
deleted file mode 100644
index 00ab41fc61c..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/context/SharedZoneRoutingContext.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.context;
-
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-
-import java.time.Instant;
-import java.util.Objects;
-
-/**
- * An implementation of {@link RoutingContext} for a zone, using {@link RoutingMethod#sharedLayer4} routing.
- *
- * @author mpolden
- */
-public class SharedZoneRoutingContext implements RoutingContext {
-
- private final ConfigServer configServer;
- private final ZoneId zone;
-
- public SharedZoneRoutingContext(ZoneId zone, ConfigServer configServer) {
- this.configServer = Objects.requireNonNull(configServer);
- this.zone = Objects.requireNonNull(zone);
- }
-
- @Override
- public void setRoutingStatus(RoutingStatus.Value value, RoutingStatus.Agent agent) {
- boolean in = value == RoutingStatus.Value.in;
- configServer.setGlobalRotationStatus(zone, in);
- }
-
- @Override
- public RoutingStatus routingStatus() {
- boolean in = configServer.getGlobalRotationStatus(zone);
- RoutingStatus.Value newValue = in ? RoutingStatus.Value.in : RoutingStatus.Value.out;
- return new RoutingStatus(newValue,
- RoutingStatus.Agent.operator,
- Instant.EPOCH); // API does not support time of change
- }
-
- @Override
- public RoutingMethod routingMethod() {
- return RoutingMethod.sharedLayer4;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/Rotation.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/Rotation.java
deleted file mode 100644
index d94124709f7..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/Rotation.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.rotation;
-
-import com.yahoo.text.Text;
-
-import java.util.Objects;
-
-/**
- * Represents a global routing rotation.
- *
- * @author mpolden
- */
-public record Rotation(RotationId id, String name) {
-
- public Rotation {
- Objects.requireNonNull(id);
- Objects.requireNonNull(name);
- }
-
- /** The ID of the allocated rotation. This value is generated by global routing system */
- public RotationId id() {
- return id;
- }
-
- /** The global rotation FQDN */
- public String name() {
- return name;
- }
-
- @Override
- public String toString() {
- return Text.format("rotation %s -> %s", id().asString(), name());
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationId.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationId.java
deleted file mode 100644
index a99c9ada0f9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationId.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.rotation;
-
-/**
- * ID of a global rotation.
- *
- * @author mpolden
- */
-public record RotationId(String id) {
-
- /** Rotation ID, e.g. rotation-42.vespa.global.routing */
- public String asString() {
- return id;
- }
-
- @Override
- public String toString() {
- return "rotation ID " + id;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationLock.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationLock.java
deleted file mode 100644
index 3043ec146a6..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationLock.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.rotation;
-
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.curator.Lock;
-
-import java.util.Objects;
-
-/**
- * A lock for the rotation repository. This is a type-safe wrapper for a curator lock.
- *
- * @author mpolden
- */
-public class RotationLock implements AutoCloseable {
-
- private final Mutex lock;
-
- RotationLock(Mutex lock) {
- this.lock = Objects.requireNonNull(lock, "lock cannot be null");
- }
-
- @Override
- public void close() {
- lock.close();
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationRepository.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationRepository.java
deleted file mode 100644
index c70826161da..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationRepository.java
+++ /dev/null
@@ -1,132 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.rotation;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.Endpoint;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import static java.util.stream.Collectors.collectingAndThen;
-
-/**
- * The rotation repository offers global rotations to Vespa applications.
- *
- * The list of rotations comes from RotationsConfig, which is set in the controller's services.xml.
- *
- * @author Oyvind Gronnesby
- * @author mpolden
- */
-public class RotationRepository {
-
- private final Map<RotationId, Rotation> allRotations;
- private final ApplicationController applications;
- private final CuratorDb curator;
-
- public RotationRepository(RotationsConfig rotationsConfig, ApplicationController applications, CuratorDb curator) {
- this.allRotations = from(rotationsConfig);
- this.applications = applications;
- this.curator = curator;
- }
-
- /** Acquire a exclusive lock for this */
- public RotationLock lock() {
- return new RotationLock(curator.lockRotations());
- }
-
- /** Get rotation with given id */
- public Rotation requireRotation(RotationId id) {
- Rotation rotation = allRotations.get(id);
- if (rotation == null) throw new IllegalArgumentException("No such rotation: '" + id.asString() + "'");
- return rotation;
- }
-
- /**
- * Returns rotation assignments for all endpoints in application.
- *
- * If rotations are already assigned, these will be returned.
- * If rotations are not assigned, a new assignment will be created taking new rotations from the repository.
- *
- * @param deploymentSpec The deployment spec of the application
- * @param instance The application requesting rotations
- * @param lock Lock which by acquired by the caller
- * @return List of rotation assignments - either new or existing
- */
- public List<AssignedRotation> getOrAssignRotations(DeploymentSpec deploymentSpec, Instance instance, RotationLock lock) {
- // Skip assignment if no rotations are configured in this system
- if (allRotations.isEmpty()) {
- return List.of();
- }
- var instanceSpec = deploymentSpec.requireInstance(instance.name());
- return assignRotationsTo(instanceSpec.endpoints(), instance, lock);
- }
-
- private List<AssignedRotation> assignRotationsTo(List<Endpoint> endpoints, Instance instance, RotationLock lock) {
- if (endpoints.isEmpty()) return List.of(); // No endpoints declared, nothing to assign.
- var availableRotations = new ArrayList<>(availableRotations(lock).values());
- var assignedRotationsByEndpointId = instance.rotations().stream()
- .collect(Collectors.toMap(AssignedRotation::endpointId,
- Function.identity()));
- var assignments = new ArrayList<AssignedRotation>();
- for (var endpoint : endpoints) {
- var endpointId = EndpointId.of(endpoint.endpointId());
- var assignedRotation = assignedRotationsByEndpointId.get(endpointId);
- RotationId rotationId;
- if (assignedRotation == null) { // No rotation is assigned to this endpoint, assign from available
- rotationId = requireNonEmpty(availableRotations).remove(0).id();
- } else { // Rotation already assigned to this endpoint, reuse it
- rotationId = assignedRotation.rotationId();
- }
- assignments.add(new AssignedRotation(ClusterSpec.Id.from(endpoint.containerId()), endpointId, rotationId, Set.copyOf(endpoint.regions())));
- }
- return Collections.unmodifiableList(assignments);
- }
-
- /**
- * Returns all unassigned rotations
- * @param lock Lock which must be acquired by the caller
- */
- public Map<RotationId, Rotation> availableRotations(@SuppressWarnings("unused") RotationLock lock) {
- List<RotationId> assignedRotations = applications.asList().stream()
- .flatMap(application -> application.instances().values().stream())
- .flatMap(instance -> instance.rotations().stream())
- .map(AssignedRotation::rotationId)
- .toList();
- Map<RotationId, Rotation> unassignedRotations = new LinkedHashMap<>(this.allRotations);
- assignedRotations.forEach(unassignedRotations::remove);
- return Collections.unmodifiableMap(unassignedRotations);
- }
-
- /** Returns a immutable map of rotation ID to rotation sorted by rotation ID */
- private static Map<RotationId, Rotation> from(RotationsConfig rotationConfig) {
- return rotationConfig.rotations().entrySet().stream()
- .map(entry -> new Rotation(new RotationId(entry.getKey()), entry.getValue().trim()))
- .sorted(Comparator.comparing(rotation -> rotation.id().asString()))
- .collect(collectingAndThen(Collectors.toMap(Rotation::id,
- rotation -> rotation,
- (k, v) -> v,
- LinkedHashMap::new),
- Collections::unmodifiableMap));
- }
-
- private static <T extends Collection<?>> T requireNonEmpty(T rotations) {
- if (rotations.isEmpty()) throw new IllegalStateException("Hosted Vespa ran out of rotations, unable to assign rotation");
- return rotations;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationState.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationState.java
deleted file mode 100644
index 53ebbd1e95e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationState.java
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.rotation;
-
-/**
- * The possible states of a global rotation.
- *
- * @author mpolden
- */
-public enum RotationState {
-
- /** Rotation has status 'in' and is receiving traffic */
- in,
-
- /** Rotation has status 'out' and is *NOT* receiving traffic */
- out,
-
- /** Rotation status is currently unknown, or no global rotation has been assigned */
- unknown
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationStatus.java
deleted file mode 100644
index 7ad841c96f9..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationStatus.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.rotation;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-
-import java.time.Instant;
-import java.util.Map;
-import java.util.Objects;
-
-/**
- * The status of all rotations assigned to an application.
- *
- * @author mpolden
- */
-public record RotationStatus(Map<RotationId, Targets> status) {
-
- public static final RotationStatus EMPTY = new RotationStatus(Map.of());
-
- public RotationStatus(Map<RotationId, Targets> status) {
- this.status = Map.copyOf(Objects.requireNonNull(status));
- }
-
- public Map<RotationId, Targets> asMap() {
- return status;
- }
-
- /** Get targets of given rotation, if any */
- public Targets of(RotationId rotation) {
- return status.getOrDefault(rotation, Targets.NONE);
- }
-
- /** Get status of deployment in given rotation, if any */
- public RotationState of(RotationId rotation, Deployment deployment) {
- return of(rotation).asMap().entrySet().stream()
- .filter(kv -> kv.getKey().equals(deployment.zone()))
- .map(Map.Entry::getValue)
- .findFirst()
- .orElse(RotationState.unknown);
- }
-
- @Override
- public String toString() {
- return "rotation status " + status;
- }
-
- public static RotationStatus from(Map<RotationId, Targets> targets) {
- return targets.isEmpty() ? EMPTY : new RotationStatus(targets);
- }
-
- /** Targets of a rotation */
- public record Targets(Map<ZoneId, RotationState> targets, Instant lastUpdated) {
-
- public static final Targets NONE = new Targets(Map.of(), Instant.EPOCH);
-
- public Targets(Map<ZoneId, RotationState> targets, Instant lastUpdated) {
- this.targets = Map.copyOf(Objects.requireNonNull(targets, "states must be non-null"));
- this.lastUpdated = Objects.requireNonNull(lastUpdated, "lastUpdated must be non-null");
- }
-
- public Map<ZoneId, RotationState> asMap() {
- return targets;
- }
-
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java
deleted file mode 100644
index 4c2f2627026..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControl.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Instant;
-import java.util.List;
-
-/**
- * Stores permissions for tenant and application resources.
- *
- * The signatures use vague types, and the exact types is a contract between this and the
- * {@link AccessControlRequests} generating data consumed by this.
- *
- * @author jonmv
- */
-public interface AccessControl {
-
- /**
- * Sets up access control based on the given credentials, and returns a tenant, based on the given specification.
- *
- * @param tenantSpec specification for the tenant to create
- * @param createdAt instant when the tenant was created
- * @param credentials the credentials for the entity requesting the creation
- * @param existing list of existing tenants, to check for conflicts
- * @return the created tenant, for keeping
- */
- Tenant createTenant(TenantSpec tenantSpec, Instant createdAt, Credentials credentials, List<Tenant> existing);
-
- /**
- * Modifies access control based on the given credentials, and returns a modified tenant, based on the given specification.
- *
- * @param tenantSpec specification for the tenant to update
- * @param credentials the credentials for the entity requesting the update
- * @param existing list of existing tenants, to check for conflicts
- * @param applications list of applications this tenant already owns
- * @return the updated tenant, for keeping
- */
- Tenant updateTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing, List<Application> applications);
-
- /**
- * Deletes access control for the given tenant.
- *
- * @param tenant the tenant to delete
- * @param credentials the credentials for the entity requesting the deletion
- */
- void deleteTenant(TenantName tenant, Credentials credentials);
-
- /**
- * Sets up access control for the given application, based on the given credentials.
- *
- * @param id the ID of the application to create
- * @param credentials the credentials for the entity requesting the creation
- */
- void createApplication(TenantAndApplicationId id, Credentials credentials);
-
- /**
- * Deletes access control for the given tenant.
- *
- * @param id the ID of the application to delete
- * @param credentials the credentials for the entity requesting the deletion
- */
- void deleteApplication(TenantAndApplicationId id, Credentials credentials);
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControlRequests.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControlRequests.java
deleted file mode 100644
index 081c72f7e25..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AccessControlRequests.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.jdisc.http.HttpRequest;
-import com.yahoo.slime.Inspector;
-
-/**
- * Extracts {@link TenantSpec}s and {@link Credentials}s from HTTP requests, to be stored in an {@link AccessControl}.
- *
- * @author jonmv
- */
-public interface AccessControlRequests {
-
- /** Extracts claim data for a tenant, from the given request. */
- TenantSpec specification(TenantName tenant, Inspector requestObject);
-
- /** Extracts credentials required for an access control modification for the given tenant, from the given request. */
- Credentials credentials(TenantName tenant, Inspector requestObject, HttpRequest jDiscRequest);
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzAccessControlRequests.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzAccessControlRequests.java
deleted file mode 100644
index ccf3db5d204..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzAccessControlRequests.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.jdisc.http.HttpRequest;
-import com.yahoo.slime.Inspector;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.TenantController;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-
-import java.security.Principal;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Extracts access control data for Athenz or user tenants from HTTP requests.
- *
- * @author jonmv
- */
-public class AthenzAccessControlRequests implements AccessControlRequests {
-
- private final TenantController tenants;
-
- @Inject
- public AthenzAccessControlRequests(Controller controller) {
- this.tenants = controller.tenants();
- }
-
- @Override
- public TenantSpec specification(TenantName tenant, Inspector requestObject) {
- return new AthenzTenantSpec(tenant,
- new AthenzDomain(required("athensDomain", requestObject)),
- new Property(required("property", requestObject)),
- optional("propertyId", requestObject).map(PropertyId::new));
- }
-
- @Override
- public Credentials credentials(TenantName tenant, Inspector requestObject, HttpRequest request) {
- return new AthenzCredentials(requireAthenzPrincipal(request),
- tenants.get(tenant).map(AthenzTenant.class::cast).map(AthenzTenant::domain)
- .orElseGet(() -> new AthenzDomain(required("athensDomain", requestObject))),
- OAuthCredentials.fromOktaRequestContext(request.context()));
- }
-
- private static String required(String fieldName, Inspector object) {
- return optional(fieldName, object) .orElseThrow(() -> new IllegalArgumentException("Missing required field '" + fieldName + "'."));
- }
-
- private static Optional<String> optional(String fieldName, Inspector object) {
- return object.field(fieldName).valid() ? Optional.of(object.field(fieldName).asString()) : Optional.empty();
- }
-
- private static AthenzPrincipal requireAthenzPrincipal(HttpRequest request) {
- Principal principal = request.getUserPrincipal();
- Objects.requireNonNull(principal, "Expected a user principal");
- if ( ! (principal instanceof AthenzPrincipal))
- throw new RuntimeException(Text.format("Expected principal of type %s, got %s",
- AthenzPrincipal.class.getSimpleName(), principal.getClass().getName()));
- return (AthenzPrincipal) principal;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzCredentials.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzCredentials.java
deleted file mode 100644
index aa8ab8375b0..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzCredentials.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Like {@link Credentials}, but the entity is rather an Athenz domain, and thus contains also a
- * token which can be used to validate the user's role memberships under this domain.
- * <em>This validation is done by Athenz, not by us.</em>
- *
- * @author jonmv
- */
-public class AthenzCredentials extends Credentials {
-
- private final AthenzDomain domain;
- private final OAuthCredentials oAuthCredentials;
-
- public AthenzCredentials(AthenzPrincipal user, AthenzDomain domain, OAuthCredentials oAuthCredentials) {
- super(user);
- this.domain = requireNonNull(domain);
- this.oAuthCredentials = requireNonNull(oAuthCredentials);
- }
-
- @Override
- public AthenzPrincipal user() { return (AthenzPrincipal) super.user(); }
-
- /** Returns the Athenz domain of the tenant on whose behalf this request is made. */
- public AthenzDomain domain() { return domain; }
-
- /** Returns the OAuth credentials required for Athenz tenancy operation */
- public OAuthCredentials oAuthCredentials() { return oAuthCredentials; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzTenantSpec.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzTenantSpec.java
deleted file mode 100644
index 70799250773..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/AthenzTenantSpec.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-
-import java.util.Optional;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Extends the specification for creating an Athenz tenant.
- *
- * @author jonmv
- */
-public class AthenzTenantSpec extends TenantSpec {
-
- private final AthenzDomain domain;
- private final Property property;
- private final Optional<PropertyId> propertyId;
-
- public AthenzTenantSpec(TenantName tenant, AthenzDomain domain, Property property, Optional<PropertyId> propertyId) {
- super(tenant);
- this.domain = domain;
- this.property = requireNonNull(property);
- this.propertyId = requireNonNull(propertyId);
- }
-
- /** The domain to create this tenant under. */
- public AthenzDomain domain() { return domain; }
-
- /** The property name of the tenant. */
- public Property property() { return property; }
-
- /** The ID of the property of the tenant. */
- public Optional<PropertyId> propertyId() { return propertyId; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Auth0Credentials.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Auth0Credentials.java
deleted file mode 100644
index aaf2b5a9367..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Auth0Credentials.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-
-import java.security.Principal;
-import java.util.Collections;
-import java.util.Set;
-
-/**
- * Like {@link Credentials}, but we know the principal is authenticated by Auth0.
- * Also includes the set of roles for which the principal is a member.
- *
- * @author andreer
- */
-public class Auth0Credentials extends Credentials {
-
- private final Set<Role> roles;
-
- public Auth0Credentials(Principal user, Set<Role> roles) {
- super(user);
- this.roles = Collections.unmodifiableSet(roles);
- }
-
- /** The set of roles set in the auth0 cookie, extracted by CloudAccessControlRequests. */
- public Set<Role> getRolesFromCookie() {
- return roles;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
deleted file mode 100644
index 051298d4f8b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControl.java
+++ /dev/null
@@ -1,136 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.restapi.RestApiException;
-import com.yahoo.vespa.flags.BooleanFlag;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.IntFlag;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.user.Roles;
-import com.yahoo.vespa.hosted.controller.api.integration.user.UserId;
-import com.yahoo.vespa.hosted.controller.api.integration.user.UserManagement;
-import com.yahoo.vespa.hosted.controller.api.role.ApplicationRole;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import java.time.Instant;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.api.role.RoleDefinition.administrator;
-import static com.yahoo.vespa.hosted.controller.api.role.RoleDefinition.hostedOperator;
-import static com.yahoo.vespa.hosted.controller.api.role.RoleDefinition.hostedSupporter;
-
-/**
- * @author jonmv
- * @author andreer
- */
-public class CloudAccessControl implements AccessControl {
-
- private final UserManagement userManagement;
- private final BooleanFlag enablePublicSignup;
- private final IntFlag maxTrialTenants;
- private final BillingController billingController;
-
- @Inject
- public CloudAccessControl(UserManagement userManagement, FlagSource flagSource, ServiceRegistry serviceRegistry) {
- this.userManagement = userManagement;
- this.enablePublicSignup = PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.bindTo(flagSource);
- this.maxTrialTenants = PermanentFlags.MAX_TRIAL_TENANTS.bindTo(flagSource);
- billingController = serviceRegistry.billingController();
- }
-
- @Override
- public CloudTenant createTenant(TenantSpec tenantSpec, Instant createdAt, Credentials credentials, List<Tenant> existing) {
- requireTenantCreationAllowed((Auth0Credentials) credentials);
- requireTenantTrialLimitNotReached(existing);
-
- CloudTenantSpec spec = (CloudTenantSpec) tenantSpec;
- CloudTenant tenant = CloudTenant.create(spec.tenant(), createdAt, credentials.user());
-
- for (Role role : Roles.tenantRoles(spec.tenant())) {
- userManagement.createRole(role);
- }
-
- var userId = List.of(new UserId(credentials.user().getName()));
- userManagement.addUsers(Role.administrator(spec.tenant()), userId);
- userManagement.addUsers(Role.developer(spec.tenant()), userId);
- userManagement.addUsers(Role.reader(spec.tenant()), userId);
-
- return tenant;
- }
-
- private void requireTenantTrialLimitNotReached(List<Tenant> existing) {
- var trialPlanId = PlanId.from("trial");
- var tenantNames = existing.stream().filter(tenant -> tenant.type() == Tenant.Type.cloud).map(Tenant::name).toList();
- var trialTenants = billingController.tenantsWithPlan(tenantNames, trialPlanId).size();
-
- if (maxTrialTenants.value() >= 0 && maxTrialTenants.value() <= trialTenants) {
- throw new RestApiException.Forbidden("Too many tenants with trial plans, please contact the Vespa support team");
- }
- }
-
- private void requireTenantCreationAllowed(Auth0Credentials auth0Credentials) {
- if (allowedByPrivilegedRole(auth0Credentials)) return;
-
- if (!allowedByFeatureFlag(auth0Credentials)) {
- throw new RestApiException.Forbidden("You are not currently permitted to create tenants. Please contact the Vespa team to request access.");
- }
-
- if(administeredTenants(auth0Credentials) >= 3) {
- throw new RestApiException.Forbidden("You are already administering 3 tenants. If you need more, please contact the Vespa team.");
- }
- }
-
- private boolean allowedByPrivilegedRole(Auth0Credentials auth0Credentials) {
- return auth0Credentials.getRolesFromCookie().stream()
- .map(Role::definition)
- .anyMatch(rd -> rd == hostedOperator || rd == hostedSupporter);
- }
-
- private boolean allowedByFeatureFlag(Auth0Credentials auth0Credentials) {
- return enablePublicSignup.with(FetchVector.Dimension.CONSOLE_USER_EMAIL, auth0Credentials.user().getName()).value();
- }
-
- private long administeredTenants(Auth0Credentials auth0Credentials) {
- // We have to verify the roles with auth0 to ensure the user is not using an "old" cookie to make too many tenants.
- return userManagement.listRoles(new UserId(auth0Credentials.user().getName())).stream()
- .map(Role::definition)
- .filter(rd -> rd == administrator)
- .count();
- }
-
- @Override
- public Tenant updateTenant(TenantSpec tenantSpec, Credentials credentials, List<Tenant> existing, List<Application> applications) {
- throw new UnsupportedOperationException("Update is not supported here, as it would entail changing the tenant name.");
- }
-
- @Override
- public void deleteTenant(TenantName tenant, Credentials credentials) {
- for (TenantRole role : Roles.tenantRoles(tenant))
- userManagement.deleteRole(role);
- }
-
- @Override
- public void createApplication(TenantAndApplicationId id, Credentials credentials) {
- for (Role role : Roles.applicationRoles(id.tenant(), id.application()))
- userManagement.createRole(role);
- }
-
- @Override
- public void deleteApplication(TenantAndApplicationId id, Credentials credentials) {
- for (ApplicationRole role : Roles.applicationRoles(id.tenant(), id.application()))
- userManagement.deleteRole(role);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControlRequests.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControlRequests.java
deleted file mode 100644
index 697b324dc3e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudAccessControlRequests.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.jdisc.http.HttpRequest;
-import com.yahoo.slime.Inspector;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * Extracts access control data for {@link CloudTenant}s from HTTP requests.
- *
- * @author jonmv
- * @author andreer
- */
-public class CloudAccessControlRequests implements AccessControlRequests {
-
- @Override
- public CloudTenantSpec specification(TenantName tenant, Inspector requestObject) {
- return new CloudTenantSpec(tenant, "token"); // TODO: remove token
- }
-
- @Override
- public Credentials credentials(TenantName tenant, Inspector requestObject, HttpRequest request) {
- return new Auth0Credentials(request.getUserPrincipal(), getUserRoles(request));
- }
-
- private static Set<Role> getUserRoles(HttpRequest request) {
- var securityContext = Optional.ofNullable(request.context().get(SecurityContext.ATTRIBUTE_NAME))
- .filter(SecurityContext.class::isInstance)
- .map(SecurityContext.class::cast)
- .orElseThrow(() -> new IllegalArgumentException("Attribute '" + SecurityContext.ATTRIBUTE_NAME + "' was not set on request"));
- return securityContext.roles();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudTenantSpec.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudTenantSpec.java
deleted file mode 100644
index f746df2b71e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudTenantSpec.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.config.provision.TenantName;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Extends the specification for creating a cloud tenant.
- *
- * @author jonmv
- */
-public class CloudTenantSpec extends TenantSpec {
-
- private final String registrationToken;
-
- public CloudTenantSpec(TenantName tenant, String registrationToken) {
- super(tenant);
- this.registrationToken = requireNonNull(registrationToken);
- }
-
- /** The cloud issued token proving the user intends to register the given tenant. */
- public String getRegistrationToken() { return registrationToken; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManager.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManager.java
deleted file mode 100644
index c2a505fc185..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManager.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.flags.LongFlag;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.TenantController;
-import com.yahoo.vespa.hosted.controller.api.integration.user.UserSessionManager;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-
-import java.time.Instant;
-
-/**
- * @author freva
- */
-public class CloudUserSessionManager implements UserSessionManager {
-
- private final TenantController tenantController;
- private final LongFlag invalidateConsoleSessions;
-
- public CloudUserSessionManager(Controller controller) {
- this.tenantController = controller.tenants();
- this.invalidateConsoleSessions = PermanentFlags.INVALIDATE_CONSOLE_SESSIONS.bindTo(controller.flagSource());
- }
-
- @Override
- public boolean shouldExpireSessionFor(SecurityContext context) {
- if (context.issuedAt().isBefore(Instant.ofEpochSecond(invalidateConsoleSessions.value())))
- return true;
-
- return context.roles().stream()
- .filter(TenantRole.class::isInstance)
- .map(TenantRole.class::cast)
- .map(TenantRole::tenant)
- .distinct()
- .anyMatch(tenantName -> shouldExpireSessionFor(tenantName, context.issuedAt()));
- }
-
- private boolean shouldExpireSessionFor(TenantName tenantName, Instant contextIssuedAt) {
- return tenantController.get(tenantName)
- .filter(CloudTenant.class::isInstance)
- .map(CloudTenant.class::cast)
- .flatMap(CloudTenant::invalidateUserSessionsBefore)
- .map(contextIssuedAt::isBefore)
- .orElse(false);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Credentials.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Credentials.java
deleted file mode 100644
index d2ad1433413..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/Credentials.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import java.security.Principal;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Credentials representing an entity for which to modify access control rules.
- *
- * @author jonmv
- */
-public class Credentials {
-
- private final Principal user;
-
- public Credentials(Principal user) {
- this.user = requireNonNull(user);
- }
-
- /** Returns the user which makes the request. */
- public Principal user() { return user; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/TenantSpec.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/TenantSpec.java
deleted file mode 100644
index 8f74c59941d..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/security/TenantSpec.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * A specification of a tenant, typically to create or modify one.
- *
- * @author jonmv
- */
-public abstract class TenantSpec {
-
- private final TenantName tenant;
-
- protected TenantSpec(TenantName tenant) {
- this.tenant = Tenant.requireName(requireNonNull(tenant));
- }
-
- /** The name of the tenant. */
- public TenantName tenant() { return tenant; }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccess.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccess.java
deleted file mode 100644
index ae1231fa450..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccess.java
+++ /dev/null
@@ -1,126 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.support.access;
-
-import java.time.Instant;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/** Immutable state of support access, keeping history of all changes/grants. */
-public class SupportAccess {
-
- public static final SupportAccess DISALLOWED_NO_HISTORY = new SupportAccess(List.of(), List.of());
-
- private final List<SupportAccessChange> changeHistory;
- private final List<SupportAccessGrant> grantHistory;
-
- /** public for serializer - do not use */
- public SupportAccess(List<SupportAccessChange> changeHistory, List<SupportAccessGrant> grantHistory) {
- this.changeHistory = Collections.unmodifiableList(changeHistory);
- this.grantHistory = Collections.unmodifiableList(grantHistory);
- }
-
- public List<SupportAccessChange> changeHistory() {
- return changeHistory;
- }
-
- public List<SupportAccessGrant> grantHistory() {
- return grantHistory;
- }
-
- public CurrentStatus currentStatus(Instant now) {
- Optional<SupportAccessChange> latestChange = changeHistory.stream().findFirst();
-
- if (latestChange.isEmpty() || latestChange.get().accessAllowedUntil().isEmpty() || now.isAfter(latestChange.get().accessAllowedUntil().get()))
- return new CurrentStatus(State.NOT_ALLOWED, Optional.empty(), Optional.empty());
-
- return new CurrentStatus(State.ALLOWED, latestChange.get().accessAllowedUntil(), Optional.of(latestChange.get().madeBy()));
- }
-
- public SupportAccess withAllowedUntil(Instant until, String changedBy, Instant changeTime) {
- if (!until.isAfter(changeTime))
- throw new IllegalArgumentException("Support access cannot be allowed for the past");
-
- verifyChangeOrdering(changeTime);
- return new SupportAccess(
- prepend(new SupportAccessChange(Optional.of(until), changeTime, changedBy), changeHistory),
- grantHistory);
- }
-
- public SupportAccess withDisallowed(String changedBy, Instant changeTime) {
- verifyChangeOrdering(changeTime);
- return new SupportAccess(
- prepend(new SupportAccessChange(Optional.empty(), changeTime, changedBy), changeHistory),
- grantHistory);
- }
-
- public SupportAccess withGrant(SupportAccessGrant supportAccessGrant) {
- return new SupportAccess(changeHistory, prepend(supportAccessGrant, grantHistory));
- }
-
- private void verifyChangeOrdering(Instant changeTime) {
- changeHistory.stream().findFirst().ifPresent(lastChange -> {
- if (changeTime.isBefore(lastChange.changeTime())) {
- throw new IllegalArgumentException("Support access change cannot be dated before previous change");
- }
- });
- }
-
- private <T> List<T> prepend(T newEntry, List<T> existingEntries) {
- return Stream.concat(Stream.of(newEntry), existingEntries.stream()) // latest change first
- .toList();
- }
-
- public static class CurrentStatus {
- private final State state;
- private final Optional<Instant> allowedUntil;
- private final Optional<String> allowedBy;
-
- private CurrentStatus(State state, Optional<Instant> allowedUntil, Optional<String> allowedBy) {
- this.state = state;
- this.allowedUntil = allowedUntil;
- this.allowedBy = allowedBy;
- }
-
- public State state() {
- return state;
- }
-
- public Optional<Instant> allowedUntil() {
- return allowedUntil;
- }
-
- public Optional<String> allowedBy() {
- return allowedBy;
- }
- }
-
- public enum State {
- NOT_ALLOWED,
- ALLOWED
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- SupportAccess that = (SupportAccess) o;
- return changeHistory.equals(that.changeHistory) && grantHistory.equals(that.grantHistory);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(changeHistory, grantHistory);
- }
-
- @Override
- public String toString() {
- return "SupportAccess{" +
- "changeHistory=" + changeHistory +
- ", grantHistory=" + grantHistory +
- '}';
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessChange.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessChange.java
deleted file mode 100644
index 6b6c869d400..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessChange.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.support.access;
-
-import java.time.Instant;
-import java.util.Objects;
-import java.util.Optional;
-
-/** An (immutable) change in support access, recording what change was made, when, and by whom. */
-public class SupportAccessChange {
- private final Instant madeAt;
- private final Optional<Instant> accessAllowedUntil;
- private final String changedBy;
-
- public SupportAccessChange(Optional<Instant> accessAllowedUntil, Instant changeTime, String changedBy) {
- this.madeAt = changeTime;
- this.accessAllowedUntil = accessAllowedUntil;
- this.changedBy = changedBy;
- }
-
- public Instant changeTime() {
- return madeAt;
- }
-
- public Optional<Instant> accessAllowedUntil() {
- return accessAllowedUntil;
- }
-
- public String madeBy() {
- return changedBy;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- SupportAccessChange that = (SupportAccessChange) o;
- return madeAt.equals(that.madeAt) && accessAllowedUntil.equals(that.accessAllowedUntil) && changedBy.equals(that.changedBy);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(madeAt, accessAllowedUntil, changedBy);
- }
-
- @Override
- public String toString() {
- return "SupportAccessChange{" +
- "madeAt=" + madeAt +
- ", accessAllowedUntil=" + accessAllowedUntil +
- ", changedBy='" + changedBy + '\'' +
- '}';
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java
deleted file mode 100644
index 7e3dc77822f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessControl.java
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.support.access;
-
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.curator.Lock;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-
-import java.security.cert.X509Certificate;
-import java.time.Instant;
-import java.time.Period;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.support.access.SupportAccess.State.ALLOWED;
-import static com.yahoo.vespa.hosted.controller.support.access.SupportAccess.State.NOT_ALLOWED;
-
-/**
- * Which application endpoints should Vespa support be allowed to access for debugging?
- *
- * @author andreer
- */
-public class SupportAccessControl {
-
- private final Controller controller;
-
- private final java.time.Period MAX_SUPPORT_ACCESS_TIME = Period.ofDays(10);
-
- public SupportAccessControl(Controller controller) {
- this.controller = controller;
- }
-
- public SupportAccess forDeployment(DeploymentId deploymentId) {
- return controller.curator().readSupportAccess(deploymentId);
- }
-
- public SupportAccess disallow(DeploymentId deployment, String by) {
- try (Mutex lock = controller.curator().lockSupportAccess(deployment)) {
- var now = controller.clock().instant();
- SupportAccess supportAccess = forDeployment(deployment);
- if (supportAccess.currentStatus(now).state() == NOT_ALLOWED) {
- throw new IllegalArgumentException("Support access is no longer allowed");
- } else {
- var disallowed = supportAccess.withDisallowed(by, now);
- controller.curator().writeSupportAccess(deployment, disallowed);
- return disallowed;
- }
- }
- }
-
- public SupportAccess allow(DeploymentId deployment, Instant until, String by) {
- try (Mutex lock = controller.curator().lockSupportAccess(deployment)) {
- var now = controller.clock().instant();
- if (until.isAfter(now.plus(MAX_SUPPORT_ACCESS_TIME))) {
- throw new IllegalArgumentException("Support access cannot be allowed for more than 10 days");
- }
- SupportAccess allowed = forDeployment(deployment).withAllowedUntil(until, by, now);
- controller.curator().writeSupportAccess(deployment, allowed);
- return allowed;
- }
- }
-
- public SupportAccess registerGrant(DeploymentId deployment, String by, X509Certificate certificate) {
- try (Mutex lock = controller.curator().lockSupportAccess(deployment)) {
- var now = controller.clock().instant();
- SupportAccess supportAccess = forDeployment(deployment);
- if (certificate.getNotAfter().toInstant().isBefore(now)) {
- throw new IllegalArgumentException("Support access certificate has already expired!");
- }
- if (certificate.getNotAfter().toInstant().isAfter(now.plus(MAX_SUPPORT_ACCESS_TIME))) {
- throw new IllegalArgumentException("Support access certificate validity time is limited to " + MAX_SUPPORT_ACCESS_TIME);
- }
- if (supportAccess.currentStatus(now).state() == NOT_ALLOWED) {
- throw new IllegalArgumentException("Support access is not currently allowed by " + deployment.toUserFriendlyString());
- }
- SupportAccess granted = supportAccess.withGrant(new SupportAccessGrant(by, certificate));
- controller.curator().writeSupportAccess(deployment, granted);
- return granted;
- }
- }
-
- public List<SupportAccessGrant> activeGrantsFor(DeploymentId deployment) {
- var now = controller.clock().instant();
- SupportAccess supportAccess = forDeployment(deployment);
- if (supportAccess.currentStatus(now).state() == NOT_ALLOWED) return List.of();
-
- return supportAccess.grantHistory().stream()
- .filter(grant -> now.isAfter(grant.certificate().getNotBefore().toInstant()))
- .filter(grant -> now.isBefore(grant.certificate().getNotAfter().toInstant()))
- .toList();
- }
-
- public boolean allowDataplaneMembership(AthenzUser identity, DeploymentId deploymentId) {
- Instant instant = controller.clock().instant();
- SupportAccess supportAccess = forDeployment(deploymentId);
- SupportAccess.CurrentStatus currentStatus = supportAccess.currentStatus(instant);
- if(currentStatus.state() == ALLOWED) {
- return controller.serviceRegistry().accessControlService().approveDataPlaneAccess(identity, currentStatus.allowedUntil().orElse(instant.plus(MAX_SUPPORT_ACCESS_TIME)));
- } else {
- return false;
- }
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessGrant.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessGrant.java
deleted file mode 100644
index ee57f14c71b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/support/access/SupportAccessGrant.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.support.access;
-
-import java.security.cert.X509Certificate;
-import java.util.Objects;
-
-public class SupportAccessGrant {
- private final String requestor;
- private final X509Certificate certificate;
-
- public SupportAccessGrant(String requestor, X509Certificate certificate) {
- this.requestor = Objects.requireNonNull(requestor);
- this.certificate = Objects.requireNonNull(certificate);
- }
-
- public String requestor() {
- return requestor;
- }
-
- public X509Certificate certificate() {
- return certificate;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- SupportAccessGrant that = (SupportAccessGrant) o;
- return requestor.equals(that.requestor) && certificate.equals(that.certificate);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(requestor, certificate);
- }
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/ControllerSslContextFactoryProvider.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/ControllerSslContextFactoryProvider.java
deleted file mode 100644
index a91b5ad72ed..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/ControllerSslContextFactoryProvider.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tls;
-
-import com.google.common.collect.Sets;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.container.jdisc.secretstore.SecretStore;
-import com.yahoo.jdisc.http.ssl.impl.TlsContextBasedProvider;
-import com.yahoo.security.KeyStoreBuilder;
-import com.yahoo.security.KeyStoreType;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.security.SslContextBuilder;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.security.tls.DefaultTlsContext;
-import com.yahoo.security.tls.PeerAuthentication;
-import com.yahoo.security.tls.TlsContext;
-import com.yahoo.vespa.hosted.controller.tls.config.TlsConfig;
-
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.security.KeyStore;
-import java.security.PrivateKey;
-import java.security.cert.X509Certificate;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**
- * Configures the controller's HTTPS connector with certificate and private key from a secret store.
- *
- * @author mpolden
- * @author bjorncs
- */
-@SuppressWarnings("unused") // Injected
-public class ControllerSslContextFactoryProvider extends TlsContextBasedProvider {
-
- private final KeyStore truststore;
- private final KeyStore keystore;
- private final Map<Integer, TlsContext> tlsContextMap = new ConcurrentHashMap<>();
-
- @Inject
- public ControllerSslContextFactoryProvider(SecretStore secretStore, TlsConfig config) {
- if (!Files.isReadable(Paths.get(config.caTrustStore()))) {
- throw new IllegalArgumentException("CA trust store file is not readable: " + config.caTrustStore());
- }
- // Trust store containing CA trust store from file
- this.truststore = KeyStoreBuilder.withType(KeyStoreType.JKS)
- .fromFile(Paths.get(config.caTrustStore()))
- .build();
-
- TlsCredentials tlsCredentials = latestValidCredentials(secretStore, config);
-
- // Key store containing key pair from secret store
- this.keystore = KeyStoreBuilder.withType(KeyStoreType.JKS)
- .withKeyEntry(getClass().getSimpleName(), tlsCredentials.privateKey, tlsCredentials.certificates)
- .build();
- }
-
- @Override
- protected TlsContext getTlsContext(String containerId, int port) {
- return tlsContextMap.computeIfAbsent(port, this::createTlsContext);
- }
-
- private TlsContext createTlsContext(int port) {
- return new DefaultTlsContext(
- new SslContextBuilder()
- .withKeyStore(keystore, new char[0])
- .withTrustStore(truststore)
- .build(),
- port != 443 ? PeerAuthentication.WANT : PeerAuthentication.DISABLED);
- }
-
- record TlsCredentials(List<X509Certificate> certificates, PrivateKey privateKey){}
-
- private static TlsCredentials latestValidCredentials(SecretStore secretStore, TlsConfig tlsConfig) {
- int version = latestVersionInSecretStore(secretStore, tlsConfig);
- return new TlsCredentials(certificates(secretStore, tlsConfig, version), privateKey(secretStore, tlsConfig, version));
- }
-
- private static int latestVersionInSecretStore(SecretStore secretStore, TlsConfig tlsConfig) {
- var certVersions = new HashSet<>(secretStore.listSecretVersions(tlsConfig.certificateSecret()));
- var keyVersions = new HashSet<>(secretStore.listSecretVersions(tlsConfig.privateKeySecret()));
- return Sets.intersection(certVersions, keyVersions).stream().mapToInt(Integer::intValue).max().orElseThrow(
- () -> new RuntimeException("No valid certificate versions found in secret store!")
- );
- }
-
- /** Get private key from secret store **/
- private static PrivateKey privateKey(SecretStore secretStore, TlsConfig config, int version) {
- return KeyUtils.fromPemEncodedPrivateKey(secretStore.getSecret(config.privateKeySecret(), version));
- }
-
- /**
- * Get certificate from secret store. If certificate secret contains multiple certificates, e.g. intermediate
- * certificates, the entire chain will be read
- */
- private static List<X509Certificate> certificates(SecretStore secretStore, TlsConfig config, int version) {
- return X509CertificateUtils.certificateListFromPem(secretStore.getSecret(config.certificateSecret(), version));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/package-info.java
deleted file mode 100644
index be84cfdfca0..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/tls/package-info.java
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-/**
- * @author mpolden
- */
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.tls;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/CertifiedOsVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/CertifiedOsVersion.java
deleted file mode 100644
index 0a790be1ab8..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/CertifiedOsVersion.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.component.Version;
-
-import java.util.Comparator;
-import java.util.Objects;
-
-/**
- * An OS version that has been certified to work on a specific Vespa version.
- *
- * @author mpolden
- */
-public record CertifiedOsVersion(OsVersion osVersion, Version vespaVersion) implements Comparable<CertifiedOsVersion> {
-
- private static final Comparator<CertifiedOsVersion> comparator = Comparator.comparing(CertifiedOsVersion::osVersion)
- .thenComparing(CertifiedOsVersion::vespaVersion);
-
- public CertifiedOsVersion {
- Objects.requireNonNull(osVersion);
- Objects.requireNonNull(vespaVersion);
- }
-
- @Override
- public int compareTo(CertifiedOsVersion that) {
- return comparator.compare(this, that);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java
deleted file mode 100644
index 4e4f00e6d4b..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/DeploymentStatistics.java
+++ /dev/null
@@ -1,145 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.component.Version;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatus;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentStatusList;
-import com.yahoo.vespa.hosted.controller.deployment.JobList;
-import com.yahoo.vespa.hosted.controller.deployment.JobStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-import java.util.stream.Stream;
-
-import static java.util.Comparator.naturalOrder;
-import static java.util.function.Function.identity;
-
-/**
- * Statistics about deployments on a platform version.
- *
- * @param version the version these statistics are for
- * @param failingUpgrades the runs on the version of this, for currently failing instances, where the failure may be because of the upgrade
- * @param otherFailing all other failing runs on the version of this, for currently failing instances
- * @param productionSuccesses the production runs where the last success was on the version of this
- * @param runningUpgrade the currently running runs on the version of this, where an upgrade is attempted
- * @param otherRunning all other currently running runs on the version on this
- *
- * @author jonmv
- */
-public record DeploymentStatistics(Version version,
- List<Run> failingUpgrades,
- List<Run> otherFailing,
- List<Run> productionSuccesses,
- List<Run> runningUpgrade,
- List<Run> otherRunning) {
-
- public DeploymentStatistics(Version version, List<Run> failingUpgrades, List<Run> otherFailing,
- List<Run> productionSuccesses, List<Run> runningUpgrade, List<Run> otherRunning) {
- this.version = Objects.requireNonNull(version);
- this.failingUpgrades = List.copyOf(failingUpgrades);
- this.otherFailing = List.copyOf(otherFailing);
- this.productionSuccesses = List.copyOf(productionSuccesses);
- this.runningUpgrade = List.copyOf(runningUpgrade);
- this.otherRunning = List.copyOf(otherRunning);
- }
-
- public static List<DeploymentStatistics> compute(Collection<Version> infrastructureVersions, DeploymentStatusList statuses) {
- Set<Version> allVersions = new HashSet<>(infrastructureVersions);
- Map<Version, List<Run>> failingUpgrade = new HashMap<>();
- Map<Version, List<Run>> otherFailing = new HashMap<>();
- Map<Version, List<Run>> productionSuccesses = new HashMap<>();
- Map<Version, List<Run>> runningUpgrade = new HashMap<>();
- Map<Version, List<Run>> otherRunning = new HashMap<>();
-
- for (DeploymentStatus status : statuses.asList()) {
- if (status.application().projectId().isEmpty())
- continue;
-
- for (Instance instance : status.application().instances().values())
- for (Deployment deployment : instance.productionDeployments().values())
- allVersions.add(deployment.version());
-
- JobList failing = status.jobs().failingHard();
-
- // Add all unsuccessful runs for failing production jobs as any run may have resulted in an incomplete deployment
- // where a subset of nodes has upgraded.
- failing.not().failingApplicationChange()
- .production()
- .mapToList(JobStatus::runs)
- .forEach(runs -> runs.descendingMap().values().stream()
- .dropWhile(run -> ! run.hasEnded())
- .takeWhile(run -> run.hasFailed())
- .forEach(run -> {
- failingUpgrade.putIfAbsent(run.versions().targetPlatform(), new ArrayList<>());
- if (failingUpgrade.get(run.versions().targetPlatform()).stream().noneMatch(existing -> existing.id().job().equals(run.id().job())))
- failingUpgrade.get(run.versions().targetPlatform()).add(run);
- }));
-
- // Add only the last failing run for test jobs.
- failing.not().failingApplicationChange()
- .not().production()
- .lastCompleted().asList()
- .forEach(run -> {
- failingUpgrade.putIfAbsent(run.versions().targetPlatform(), new ArrayList<>());
- failingUpgrade.get(run.versions().targetPlatform()).add(run);
- });
-
- // Add only the last failing for instances failing only an application change, i.e., no upgrade.
- failing.failingApplicationChange()
- .lastCompleted().asList()
- .forEach(run -> {
- otherFailing.putIfAbsent(run.versions().targetPlatform(), new ArrayList<>());
- otherFailing.get(run.versions().targetPlatform()).add(run);
- });
-
- status.jobs().production()
- .lastSuccess().asList()
- .forEach(run -> {
- productionSuccesses.putIfAbsent(run.versions().targetPlatform(), new ArrayList<>());
- productionSuccesses.get(run.versions().targetPlatform()).add(run);
- });
-
- JobList running = status.jobs().running();
- running.upgrading()
- .lastTriggered().asList()
- .forEach(run -> {
- runningUpgrade.putIfAbsent(run.versions().targetPlatform(), new ArrayList<>());
- runningUpgrade.get(run.versions().targetPlatform()).add(run);
- });
-
- running.not().upgrading()
- .lastTriggered().asList()
- .forEach(run -> {
- otherRunning.putIfAbsent(run.versions().targetPlatform(), new ArrayList<>());
- otherRunning.get(run.versions().targetPlatform()).add(run);
- });
- }
-
- return Stream.of(allVersions.stream(),
- failingUpgrade.keySet().stream(),
- otherFailing.keySet().stream(),
- productionSuccesses.keySet().stream(),
- runningUpgrade.keySet().stream(),
- otherRunning.keySet().stream())
- .flatMap(identity()) // Lol.
- .distinct()
- .sorted(naturalOrder())
- .map(version -> new DeploymentStatistics(version,
- failingUpgrade.getOrDefault(version, List.of()),
- otherFailing.getOrDefault(version, List.of()),
- productionSuccesses.getOrDefault(version, List.of()),
- runningUpgrade.getOrDefault(version, List.of()),
- otherRunning.getOrDefault(version, List.of())))
- .toList();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/MavenRepositoryClient.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/MavenRepositoryClient.java
deleted file mode 100644
index e0d6dcfe36e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/MavenRepositoryClient.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.vespa.hosted.controller.api.integration.maven.ArtifactId;
-import com.yahoo.vespa.hosted.controller.api.integration.maven.MavenRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.maven.Metadata;
-import com.yahoo.vespa.hosted.controller.maven.repository.config.MavenRepositoryConfig;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * Http client implementation of a {@link MavenRepository}, which uses a configured repository and artifact ID.
- *
- * @author jonmv
- */
-public class MavenRepositoryClient implements MavenRepository {
-
- private final HttpClient client;
- private final URI apiUrl;
- private final ArtifactId id;
-
- public MavenRepositoryClient(MavenRepositoryConfig config) {
- this.client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build();
- this.apiUrl = URI.create(config.apiUrl() + "/").normalize();
- this.id = new ArtifactId(config.groupId(), config.artifactId());
- }
-
- @Override
- public Metadata metadata() {
- try {
- HttpRequest request = HttpRequest.newBuilder(withArtifactPath(apiUrl, id)).build();
- HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(UTF_8));
- if (response.statusCode() != 200)
- throw new RuntimeException("Status code '" + response.statusCode() + "' and body\n'''\n" +
- response.body() + "\n'''\nfor request " + request);
-
- return Metadata.fromXml(response.body());
- }
- catch (IOException | InterruptedException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public ArtifactId artifactId() {
- return id;
- }
-
- static URI withArtifactPath(URI baseUrl, ArtifactId id) {
- List<String> parts = new ArrayList<>(List.of(id.groupId().split("\\.")));
- parts.add(id.artifactId());
- parts.add("maven-metadata.xml");
- return baseUrl.resolve(String.join("/", parts));
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java
deleted file mode 100644
index c3b8a825cb8..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/NodeVersion.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Objects;
-import java.util.Optional;
-
-/**
- * Version information for a node allocated to a {@link com.yahoo.vespa.hosted.controller.application.SystemApplication}.
- *
- * @author mpolden
- */
-public record NodeVersion(HostName hostname,
- ZoneId zone,
- Version currentVersion,
- Version wantedVersion,
- Optional<Instant> suspendedAt) {
-
- public NodeVersion {
- Objects.requireNonNull(hostname, "hostname must be non-null");
- Objects.requireNonNull(zone, "zone must be non-null");
- Objects.requireNonNull(currentVersion, "version must be non-null");
- Objects.requireNonNull(wantedVersion, "wantedVersion must be non-null");
- Objects.requireNonNull(suspendedAt, "suspendedAt must be non-null");
- }
-
- /** Returns the duration of the change in this, measured relative to instant */
- public Duration changeDuration(Instant instant) {
- if (!upgrading()) return Duration.ZERO;
- if (suspendedAt.isEmpty()) return Duration.ZERO; // Node hasn't suspended to apply the change yet
- return Duration.between(suspendedAt.get(), instant).abs();
- }
-
- @Override
- public String toString() {
- return hostname + ": " + currentVersion.toFullString() + " -> " + wantedVersion.toFullString() +
- " [zone=" + zone + ", suspendedAt=" + suspendedAt.map(Instant::toString)
- .orElse("<not suspended>") + "]";
- }
-
- /** Returns whether this is upgrading */
- private boolean upgrading() {
- return currentVersion.isBefore(wantedVersion);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersion.java
deleted file mode 100644
index 68b3b01f75a..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersion.java
+++ /dev/null
@@ -1,35 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-
-import java.util.Comparator;
-import java.util.Objects;
-
-/**
- * An OS version for a cloud in this system.
- *
- * @author mpolden
- */
-public record OsVersion(Version version, CloudName cloud) implements Comparable<OsVersion> {
-
- private static final Comparator<OsVersion> comparator = Comparator.comparing(OsVersion::cloud)
- .thenComparing(OsVersion::version);
-
- public OsVersion {
- Objects.requireNonNull(version, "version must be non-null");
- Objects.requireNonNull(cloud, "cloud must be non-null");
- }
-
- @Override
- public String toString() {
- return "version " + version.toFullString() + " for " + cloud + " cloud";
- }
-
- @Override
- public int compareTo(OsVersion that) {
- return comparator.compare(this, that);
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java
deleted file mode 100644
index f031b906dc0..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionStatus.java
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.google.common.collect.ImmutableMap;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.zone.UpgradePolicy;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.maintenance.OsUpgrader;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-/**
- * Information about OS versions in this system.
- *
- * @author mpolden
- */
-public record OsVersionStatus(Map<OsVersion, List<NodeVersion>> versions) {
-
- public static final OsVersionStatus empty = new OsVersionStatus(ImmutableMap.of());
-
- /** Public for serialization purpose only. Use {@link OsVersionStatus#compute(Controller)} for an up-to-date status */
- public OsVersionStatus(Map<OsVersion, List<NodeVersion>> versions) {
- this.versions = ImmutableMap.copyOf(Objects.requireNonNull(versions, "versions must be non-null"));
- }
-
- /** Returns all node versions that exist in given cloud */
- public List<NodeVersion> nodesIn(CloudName cloud) {
- List<NodeVersion> nodeVersions = new ArrayList<>();
- versions.forEach((osVersion, nodesOnVersion) -> {
- if (osVersion.cloud().equals(cloud)) {
- nodeVersions.addAll(nodesOnVersion);
- }
- });
- return Collections.unmodifiableList(nodeVersions);
- }
-
- /** Returns versions that exist in given cloud */
- public Set<Version> versionsIn(CloudName cloud) {
- return versions.keySet().stream()
- .filter(osVersion -> osVersion.cloud().equals(cloud))
- .map(OsVersion::version)
- .collect(Collectors.toUnmodifiableSet());
- }
-
- /** Compute the current OS versions in this system. This is expensive and should be called infrequently */
- public static OsVersionStatus compute(Controller controller) {
- Map<OsVersion, List<NodeVersion>> osVersions = new HashMap<>();
- controller.os().targets().forEach(target -> osVersions.put(target.osVersion(), new ArrayList<>()));
-
- for (var application : SystemApplication.all()) {
- for (var zone : zonesToUpgrade(controller)) {
- if (!application.shouldUpgradeOs()) continue;
- Version targetOsVersion = controller.serviceRegistry().configServer().nodeRepository()
- .targetVersionsOf(zone.getVirtualId())
- .osVersion(application.nodeType())
- .orElse(Version.emptyVersion);
-
- for (var node : controller.serviceRegistry().configServer().nodeRepository().list(zone.getVirtualId(), NodeFilter.all().applications(application.id()))) {
- if (!OsUpgrader.canUpgrade(node, true)) continue;
- Optional<Instant> suspendedAt = node.suspendedSince();
- NodeVersion nodeVersion = new NodeVersion(node.hostname(), zone.getVirtualId(), node.currentOsVersion(),
- targetOsVersion, suspendedAt);
- OsVersion osVersion = new OsVersion(nodeVersion.currentVersion(), zone.getCloudName());
- osVersions.computeIfAbsent(osVersion, (k) -> new ArrayList<>())
- .add(nodeVersion);
- }
- }
- }
-
- return new OsVersionStatus(osVersions);
- }
-
- private static List<ZoneApi> zonesToUpgrade(Controller controller) {
- return controller.zoneRegistry().osUpgradePolicies().stream()
- .flatMap(upgradePolicy -> upgradePolicy.steps().stream())
- .map(UpgradePolicy.Step::zones)
- .flatMap(Collection::stream)
- .toList();
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionTarget.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionTarget.java
deleted file mode 100644
index ea9322b5fab..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/OsVersionTarget.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.component.Version;
-
-import java.time.Instant;
-import java.util.Objects;
-
-/**
- * The OS version target for a cloud and the time it was scheduled.
- *
- * @author mpolden
- */
-public record OsVersionTarget(OsVersion osVersion, Instant scheduledAt, boolean pinned, boolean downgrade) implements VersionTarget, Comparable<OsVersionTarget> {
-
- public OsVersionTarget {
- Objects.requireNonNull(osVersion);
- Objects.requireNonNull(scheduledAt);
- }
-
- @Override
- public int compareTo(OsVersionTarget o) {
- return osVersion.compareTo(o.osVersion);
- }
-
- @Override
- public Version version() {
- return osVersion.version();
- }
-
- @Override
- public boolean downgrade() {
- return downgrade;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java
deleted file mode 100644
index 28938577876..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionStatus.java
+++ /dev/null
@@ -1,285 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.maintenance.SystemUpgrader;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.stream.Collectors;
-
-/**
- * Information about the current platform versions in use.
- * The versions in use are the set of all versions running in current applications, versions
- * of config servers in all zones, and the version of this controller itself.
- *
- * @author bratseth
- * @author mpolden
- */
-public record VersionStatus(List<VespaVersion> versions, int currentMajor) {
-
- private static final Logger log = Logger.getLogger(VersionStatus.class.getName());
-
- /** Create a version status. DO NOT USE: Public for testing and serialization only */
- public VersionStatus(List<VespaVersion> versions, int currentMajor) {
- this.versions = List.copyOf(versions);
- this.currentMajor = currentMajor;
- }
-
- /** Returns the current version of controllers in this system */
- public Optional<VespaVersion> controllerVersion() {
- return versions().stream().filter(VespaVersion::isControllerVersion).findFirst();
- }
-
- /**
- * Returns the current Vespa version of the system controlled by this,
- * or empty if we have not currently determined what the system version is in this status.
- */
- public Optional<VespaVersion> systemVersion() {
- return versions().stream().filter(VespaVersion::isSystemVersion).findFirst();
- }
-
- /** Returns whether the system is currently upgrading */
- public boolean isUpgrading() {
- return systemVersion().map(VespaVersion::versionNumber).orElse(Version.emptyVersion)
- .isBefore(controllerVersion().map(VespaVersion::versionNumber)
- .orElse(Version.emptyVersion));
- }
-
- /**
- * Lists all currently active Vespa versions, with deployment statistics,
- * sorted from lowest to highest version number.
- * The returned list is immutable.
- * Calling this is free, but the returned status is slightly out of date.
- */
- public List<VespaVersion> versions() { return versions; }
-
- /** Lists all currently active Vespa versions, from lowest to highest number, which are not newer than the system version. */
- public List<VespaVersion> deployableVersions() {
- List<VespaVersion> deployable = new ArrayList<>();
- for (VespaVersion version : versions) {
- deployable.add(version);
- if (version.isSystemVersion())
- return deployable;
- }
- return List.of();
- }
-
- /** Returns the given version, or null if it is not present */
- public VespaVersion version(Version version) {
- return versions.stream().filter(v -> v.versionNumber().equals(version)).findFirst().orElse(null);
- }
-
- /** Returns whether given version is active in this system */
- public boolean isActive(Version version) {
- if (version(version) != null) return true;
- // Occasionally we may deploy unofficial versions of a given Vespa version, i.e. given the version 8.42.1,
- // an unofficial version 8.42.1.a may exist. Count such versions as active if their root version is active
- Version rootVersion = new Version(version.getMajor(), version.getMinor(), version.getMicro());
- return version(rootVersion) != null;
- }
-
- /** Create the empty version status */
- public static VersionStatus empty() { return new VersionStatus(List.of(), -1); }
-
- /** Create a full, updated version status. This is expensive and should be done infrequently */
- public static VersionStatus compute(Controller controller) {
- VersionStatus versionStatus = controller.readVersionStatus();
- int currentMajor = versionStatus.currentMajor();
- List<NodeVersion> systemApplicationVersions = findSystemApplicationVersions(controller, versionStatus);
- Map<ControllerVersion, List<HostName>> controllerVersions = findControllerVersions(controller);
-
- Map<Version, List<HostName>> infrastructureVersions = new HashMap<>();
- for (var kv : controllerVersions.entrySet()) {
- infrastructureVersions.computeIfAbsent(kv.getKey().version(), (k) -> new ArrayList<>())
- .addAll(kv.getValue());
- }
- for (var nodeVersion : systemApplicationVersions) {
- infrastructureVersions.computeIfAbsent(nodeVersion.currentVersion(), (k) -> new ArrayList<>())
- .add(nodeVersion.hostname());
- }
-
- // The system version is the oldest infrastructure version, if that version is newer than the current system
- // version
- Version newSystemVersion = infrastructureVersions.keySet().stream().min(Comparator.naturalOrder()).get();
- Version systemVersion = versionStatus.systemVersion()
- .map(VespaVersion::versionNumber)
- .orElse(newSystemVersion);
- if (newSystemVersion.isBefore(systemVersion)) {
- log.warning("Refusing to lower system version from " +
- systemVersion.toFullString() +
- " to " +
- newSystemVersion.toFullString() +
- ", nodes on " + newSystemVersion.toFullString() + ": " +
- infrastructureVersions.get(newSystemVersion).stream()
- .map(HostName::value)
- .collect(Collectors.joining(", ")));
- } else {
- systemVersion = newSystemVersion;
- }
-
- Set<Version> allVersions = new HashSet<>(infrastructureVersions.keySet());
- for (Application application : controller.applications().asList())
- for (Instance instance : application.instances().values())
- for (Deployment deployment : instance.deployments().values())
- allVersions.add(deployment.version());
-
- List<DeploymentStatistics> deploymentStatistics = DeploymentStatistics.compute(allVersions,
- controller.jobController().deploymentStatuses(ApplicationList.from(controller.applications().asList())
- .withProjectId()
- .withJobs()));
- List<VespaVersion> versions = new ArrayList<>();
- List<Version> releasedVersions = controller.mavenRepository().metadata().versions(controller.clock().instant());
-
- for (DeploymentStatistics statistics : deploymentStatistics) {
- if (statistics.version().isEmpty()) continue;
-
- try {
- boolean isReleased = Collections.binarySearch(releasedVersions, statistics.version()) >= 0;
- List<NodeVersion> nodeVersions = systemApplicationVersions.stream()
- .filter(nodeVersion -> nodeVersion.currentVersion().equals(statistics.version()))
- .toList();
- VespaVersion vespaVersion = createVersion(statistics,
- controllerVersions.keySet(),
- systemVersion,
- isReleased,
- nodeVersions,
- controller,
- versionStatus);
- versions.add(vespaVersion);
- if (vespaVersion.confidence().equalOrHigherThan(Confidence.high))
- currentMajor = Math.max(currentMajor, vespaVersion.versionNumber().getMajor());
- } catch (IllegalArgumentException e) {
- log.log(Level.WARNING, "Unable to create VespaVersion for version " +
- statistics.version().toFullString(), e);
- }
- }
-
- Collections.sort(versions);
-
- return new VersionStatus(versions, currentMajor);
- }
-
- private static List<NodeVersion> findSystemApplicationVersions(Controller controller, VersionStatus versionStatus) {
- List<NodeVersion> nodeVersions = new ArrayList<>();
- for (var zone : controller.zoneRegistry().zones().controllerUpgraded().zones()) {
- for (var application : SystemApplication.notController()) {
- var nodes = controller.serviceRegistry().configServer().nodeRepository()
- .list(zone.getId(), NodeFilter.all().applications(application.id())).stream()
- .filter(SystemUpgrader::eligibleForUpgrade)
- .toList();
- if (nodes.isEmpty()) continue;
- boolean configConverged = application.configConvergedIn(zone.getId(), controller, Optional.empty());
- if (!configConverged) {
- log.log(Level.WARNING, "Config for " + application.id() + " in " + zone.getId() +
- " has not converged");
- }
- for (var node : nodes) {
- // Only use current node version if config has converged
- var version = configConverged ? node.currentVersion() : controller.systemVersion(versionStatus);
- var nodeVersion = new NodeVersion(node.hostname(), zone.getId(), version, node.wantedVersion(),
- node.suspendedSince());
- nodeVersions.add(nodeVersion);
- }
- }
- }
- return nodeVersions;
- }
-
- private static Map<ControllerVersion, List<HostName>> findControllerVersions(Controller controller) {
- Map<ControllerVersion, List<HostName>> versions = new HashMap<>();
- if (controller.curator().cluster().isEmpty()) { // Use vtag if we do not have cluster
- versions.computeIfAbsent(ControllerVersion.CURRENT, (k) -> new ArrayList<>())
- .add(controller.hostname());
- } else {
- for (String host : controller.curator().cluster()) {
- HostName hostname = HostName.of(host);
- versions.computeIfAbsent(controller.curator().readControllerVersion(hostname), (k) -> new ArrayList<>())
- .add(hostname);
- }
- }
- return versions;
- }
-
- private static VespaVersion createVersion(DeploymentStatistics statistics,
- Set<ControllerVersion> controllerVersions,
- Version systemVersion,
- boolean isReleased,
- List<NodeVersion> nodeVersions,
- Controller controller,
- VersionStatus versionStatus) {
- ControllerVersion latestVersion = controllerVersions.stream().max(Comparator.naturalOrder()).get();
- boolean isSystemVersion = statistics.version().equals(systemVersion);
- boolean isControllerVersion = controllerVersions.size() == 1 &&
- statistics.version().equals(controllerVersions.iterator().next().version());
- VespaVersion.Confidence confidence = controller.curator().readConfidenceOverrides().get(statistics.version());
- boolean confidenceIsOverridden = confidence != null;
- VespaVersion existingVespaVersion = versionStatus.version(statistics.version());
-
- // Compute confidence
- if (!confidenceIsOverridden) {
- Confidence newConfidence = VespaVersion.confidenceFrom(statistics, controller, versionStatus);
- Confidence oldConfidence = Optional.ofNullable(versionStatus.version(statistics.version()))
- .map(VespaVersion::confidence)
- .orElse(newConfidence);
- // Always update confidence for system and controller
- // Also allow older versions to transition from normal to high confidence
- if (isSystemVersion || isControllerVersion || oldConfidence == Confidence.normal && newConfidence == Confidence.high) {
- confidence = newConfidence;
- } else {
- // Otherwise, this is an older version, so we preserve the existing confidence, if any
- confidence = oldConfidence;
- }
- }
-
- // Preserve existing commit details if we've previously computed status for this version
- var commitSha = latestVersion.commitSha();
- var commitDate = latestVersion.commitDate();
- if (existingVespaVersion != null) {
- commitSha = existingVespaVersion.releaseCommit();
- commitDate = existingVespaVersion.committedAt();
-
- // Keep existing confidence if we cannot raise it at this moment in time
- if (!confidenceIsOverridden &&
- !existingVespaVersion.confidence().canChangeTo(confidence,
- controller.serviceRegistry().zoneRegistry().system(),
- controller.clock().instant())) {
- confidence = existingVespaVersion.confidence();
- }
- }
-
- return new VespaVersion(statistics.version(),
- commitSha,
- commitDate,
- isControllerVersion,
- isSystemVersion,
- isReleased,
- nodeVersions,
- confidence);
- }
-
- /** Whether no version on a newer major, with high confidence, can be deployed. */
- public boolean isOnCurrentMajor(Version version) {
- return version.getMajor() >= currentMajor;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionTarget.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionTarget.java
deleted file mode 100644
index 6d3aac9475e..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VersionTarget.java
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.component.Version;
-
-/**
- * Interface for a version target of some kind of upgrade.
- *
- * @author mpolden
- */
-public interface VersionTarget {
-
- /** The version of this target */
- Version version();
-
- /** Returns whether this target is potentially a downgrade */
- boolean downgrade();
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java
deleted file mode 100644
index 9921102d460..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersion.java
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.InstanceList;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-
-import java.time.Instant;
-import java.time.ZoneOffset;
-import java.util.List;
-
-import static com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy;
-
-/**
- * Information about a particular Vespa version.
- *
- * Vespa versions are identified by their version number and ordered by increasing version numbers.
- *
- * @author bratseth
- */
-public record VespaVersion(Version version,
- String releaseCommit,
- Instant committedAt,
- boolean isControllerVersion,
- boolean isSystemVersion,
- boolean isReleased,
- List<NodeVersion> nodeVersions,
- Confidence confidence) implements Comparable<VespaVersion> {
-
- public static Confidence confidenceFrom(DeploymentStatistics statistics, Controller controller, VersionStatus versionStatus) {
- int thisMajorVersion = statistics.version().getMajor();
- InstanceList all = InstanceList.from(controller.jobController().deploymentStatuses(ApplicationList.from(controller.applications().asList())
- .withProductionDeployment()))
- .allowingMajorVersion(thisMajorVersion, versionStatus);
- // 'production on this': All production deployment jobs upgrading to this version have completed without failure
- InstanceList productionOnThis = all.matching(instance -> statistics.productionSuccesses().stream().anyMatch(run -> run.id().application().equals(instance)))
- .not().failingUpgrade()
- .not().upgradingTo(statistics.version());
- InstanceList failingOnThis = all.matching(instance -> statistics.failingUpgrades().stream().anyMatch(run -> run.id().application().equals(instance)));
-
- // 'broken' if any canary fails, and no non-canary is upgraded
- if ( ! failingOnThis.with(UpgradePolicy.canary).isEmpty() && productionOnThis.not().with(UpgradePolicy.canary).isEmpty())
- return Confidence.broken;
-
- // 'broken' if 6 non-canary was broken by this, and that is at least 5% of all
- if (nonCanaryApplicationsBroken(statistics.version(), failingOnThis, productionOnThis))
- return Confidence.broken;
-
- // 'low' unless all unpinned canary applications are upgraded
- if (productionOnThis.with(UpgradePolicy.canary).unpinned().size() < all.withProductionDeployment().with(UpgradePolicy.canary).unpinned().size())
- return Confidence.low;
-
- // 'low' unless at least half of all canary applications are upgraded
- if (productionOnThis.with(UpgradePolicy.canary).size() < all.withProductionDeployment().with(UpgradePolicy.canary).size() * 0.5)
- return Confidence.low;
-
- // 'high' if 90% of all unpinned default upgrade applications, and 50% of all of them, have upgraded
- if ( productionOnThis.with(UpgradePolicy.defaultPolicy).unpinned().groupingBy(TenantAndApplicationId::from).size() >=
- all.withProductionDeployment().with(UpgradePolicy.defaultPolicy).unpinned().groupingBy(TenantAndApplicationId::from).size() * 0.9
- && productionOnThis.with(UpgradePolicy.defaultPolicy).groupingBy(TenantAndApplicationId::from).size() >=
- all.withProductionDeployment().with(UpgradePolicy.defaultPolicy).groupingBy(TenantAndApplicationId::from).size() * 0.5)
- return Confidence.high;
-
- return Confidence.normal;
- }
-
- /** Returns the version number of this Vespa version */
- public Version versionNumber() { return version; }
-
- /** Returns the sha of the release tag commit for this version in git */
- public String releaseCommit() { return releaseCommit; }
-
- /** Returns the time of the release commit */
- public Instant committedAt() { return committedAt; }
-
- /** Returns whether this is the current version of controllers in this system (the lowest version across all
- * controllers) */
- public boolean isControllerVersion() {
- return isControllerVersion;
- }
-
- /**
- * Returns whether this is the current version of the infrastructure of the system
- * (i.e the lowest version across all controllers and all config servers in all zones).
- * A goal of the controllers is to eventually (limited by safety and upgrade capacity) drive
- * all applications to this version.
- *
- * Note that the self version may be higher than the current system version if
- * all config servers are not yet upgraded to the version of the controllers.
- */
- public boolean isSystemVersion() { return isSystemVersion; }
-
- /** Returns whether the artifacts of this release are available in the configured maven repository. */
- public boolean isReleased() { return isReleased; }
-
- /** Returns the versions of nodes allocated to system applications (across all zones) */
- public List<NodeVersion> nodeVersions() {
- return nodeVersions;
- }
-
- /** Returns the confidence we have in this versions suitability for production */
- public Confidence confidence() { return confidence; }
-
- @Override
- public int compareTo(VespaVersion other) {
- return this.versionNumber().compareTo(other.versionNumber());
- }
-
- @Override
- public int hashCode() { return versionNumber().hashCode(); }
-
- @Override
- public boolean equals(Object other) {
- if (other == this) return true;
- if ( ! (other instanceof VespaVersion)) return false;
- return ((VespaVersion)other).versionNumber().equals(this.versionNumber());
- }
-
- /** The confidence of a version. */
- public enum Confidence {
-
- /** Rollout was aborted. The system infrastructure should stay on, or roll back to, its current version */
- aborted,
-
- /** This version has been proven defective */
- broken,
-
- /** We don't have sufficient evidence that this version is working */
- low,
-
- /** We have sufficient evidence that this version is working */
- normal,
-
- /** This version works, but we want users to stop using it */
- legacy,
-
- /** We have overwhelming evidence that this version is working */
- high;
-
- /** Returns true if this confidence is at least as high as the given confidence */
- public boolean equalOrHigherThan(Confidence other) {
- return this.compareTo(other) >= 0;
- }
-
- /** Returns true if this can be changed to target at given instant */
- public boolean canChangeTo(Confidence target, SystemName system, Instant instant) {
- if (this.equalOrHigherThan(normal)) return true; // Confidence can always change from >= normal
- if (!target.equalOrHigherThan(normal)) return true; // Confidence can always change to < normal
-
- var hourOfDay = instant.atZone(ZoneOffset.UTC).getHour();
- var dayOfWeek = instant.atZone(ZoneOffset.UTC).getDayOfWeek();
- var hourEnd = system == SystemName.Public ? 13 : 11;
- // Confidence can only be raised between 05:00:00 and 11:59:59Z (13:59:59Z for public), and not during weekends or Friday.
- return hourOfDay >= 5 && hourOfDay <= hourEnd
- && dayOfWeek.getValue() < 5;
- }
-
- }
-
- private static boolean nonCanaryApplicationsBroken(Version version,
- InstanceList failingOnThis,
- InstanceList productionOnThis) {
- int failingNonCanaries = failingOnThis.startedFailingOn(version)
- .not().with(UpgradePolicy.canary)
- .groupingBy(TenantAndApplicationId::from).size();
- int productionNonCanaries = productionOnThis.not().with(UpgradePolicy.canary)
- .groupingBy(TenantAndApplicationId::from).size();
-
- if (productionNonCanaries + failingNonCanaries == 0) return false;
-
- // 'broken' if 6 non-canary was broken by this, and that is at least 5% of all
- return failingNonCanaries >= 6 && failingNonCanaries >= productionOnThis.groupingBy(TenantAndApplicationId::from).size() * 0.05;
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersionTarget.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersionTarget.java
deleted file mode 100644
index bf66425fe81..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/VespaVersionTarget.java
+++ /dev/null
@@ -1,24 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.component.Version;
-
-import java.util.Objects;
-
-/**
- * The target Vespa version for a system.
- *
- * @author mpolden
- */
-public record VespaVersionTarget(Version version, boolean downgrade) implements VersionTarget {
-
- public VespaVersionTarget {
- Objects.requireNonNull(version);
- }
-
- @Override
- public String toString() {
- return "vespa version target " + version.toFullString() + (downgrade ? " (downgrade)" : "");
- }
-
-}
diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/package-info.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/package-info.java
deleted file mode 100644
index 73ca7d2b42f..00000000000
--- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/versions/package-info.java
+++ /dev/null
@@ -1,5 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-@ExportPackage
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.athenz.config.athenz.def b/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.athenz.config.athenz.def
deleted file mode 100644
index c9c93705c95..00000000000
--- a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.athenz.config.athenz.def
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-namespace=vespa.hosted.controller.athenz.config
-
-# URL to ZMS API endpoint
-zmsUrl string
-
-# URL to ZTS API endpoint
-ztsUrl string
-
-# Athenz domain for controller identity. The domain is also used for Athenz tenancy integration.
-domain string
-
-# Athenz service name for controller identity
-service.name string
-
diff --git a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.controller.def b/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.controller.def
deleted file mode 100644
index fb17d33ae16..00000000000
--- a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.controller.def
+++ /dev/null
@@ -1,7 +0,0 @@
-# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-# Generic config for controller
-namespace=vespa.hosted.controller.config
-
-steprunner.testerapp.tenantCdBundle string default="cloud-tenant-cd"
-
-steprunner.testerapp.runtimeProviderClass string default="ai.vespa.hosted.cd.cloud.impl.VespaTestRuntimeProvider"
diff --git a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.core-dump-token-resealing.def b/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.core-dump-token-resealing.def
deleted file mode 100644
index 619b0ee5bdf..00000000000
--- a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.core-dump-token-resealing.def
+++ /dev/null
@@ -1,6 +0,0 @@
-# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-namespace=vespa.hosted.controller.config
-
-# Key name for private key used for re-sealing decryption tokens.
-# Using the default of "" means the resealing feature is disabled and no key will be looked up.
-resealingPrivateKeyName string default=""
diff --git a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.well-known-folder.def b/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.well-known-folder.def
deleted file mode 100644
index 2717a2753a0..00000000000
--- a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.config.well-known-folder.def
+++ /dev/null
@@ -1,5 +0,0 @@
-# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-# Config for serving content from .well-known directory
-namespace=vespa.hosted.controller.config
-
-securityTxt string
diff --git a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.maven.repository.config.maven-repository.def b/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.maven.repository.config.maven-repository.def
deleted file mode 100644
index c83d13770df..00000000000
--- a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.maven.repository.config.maven-repository.def
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-namespace=vespa.hosted.controller.maven.repository.config
-
-
-# URL to the Maven repository API that holds artifacts for tenants in the controller's system
-#
-apiUrl string default=https://repo1.maven.org/maven2/
-
-# Group ID of the artifact to list versions for
-#
-groupId string default=com.yahoo.vespa
-
-# Artifact ID of the artifact to list versions for
-#
-artifactId string default=cloud-tenant-base
diff --git a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.tls.config.tls.def b/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.tls.config.tls.def
deleted file mode 100644
index 2c3a7f9eb8b..00000000000
--- a/controller-server/src/main/resources/configdefinitions/vespa.hosted.controller.tls.config.tls.def
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-namespace=vespa.hosted.controller.tls.config
-
-# Path to the CA trust store
-caTrustStore string
-
-# Secret store key names for certificate and private key
-certificateSecret string
-privateKeySecret string
diff --git a/controller-server/src/main/resources/configdefinitions/vespa.hosted.rotation.config.rotations.def b/controller-server/src/main/resources/configdefinitions/vespa.hosted.rotation.config.rotations.def
deleted file mode 100644
index 324426e1860..00000000000
--- a/controller-server/src/main/resources/configdefinitions/vespa.hosted.rotation.config.rotations.def
+++ /dev/null
@@ -1,4 +0,0 @@
-# Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-namespace=vespa.hosted.rotation.config
-
-rotations{} string
diff --git a/controller-server/src/main/resources/mail/default-mail-content.vm b/controller-server/src/main/resources/mail/default-mail-content.vm
deleted file mode 100644
index 02de98b900d..00000000000
--- a/controller-server/src/main/resources/mail/default-mail-content.vm
+++ /dev/null
@@ -1,131 +0,0 @@
-<tbody>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <h1
- style="
- text-align: center;
- color: #000000;
- line-height: 32px;
- "
- >
- $esc.html($mailTitle)
- </h1>
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
-
- #parse($mailMessageTemplate)
-
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="center"
- vertical-align="middle"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 20px;
- padding-bottom: 20px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="border-collapse: separate; line-height: 100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- bgcolor="#005A8E"
- role="presentation"
- style="
- border: none;
- border-radius: 100px;
- cursor: auto;
- mso-padding-alt: 15px 25px 15px 25px;
- background: #005a8e;
- "
- valign="middle"
- >
- <a
- href="$consoleLink"
- style="
- display: inline-block;
- background: #005a8e;
- color: #ffffff;
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- font-weight: normal;
- line-height: 120%;
- margin: 0;
- text-decoration: none;
- text-transform: none;
- padding: 15px 25px 15px 25px;
- mso-padding-alt: 0px;
- border-radius: 100px;
- "
- target="_blank"
- ><b style="font-weight: 700"
- ><b style="font-weight: 700"
- >Go to Console</b
- ></b
- ></a
- >
- </td>
- </tr>
- </tbody>
- </table>
- </td>
-</tr>
-</tbody> \ No newline at end of file
diff --git a/controller-server/src/main/resources/mail/mail-verification.vm b/controller-server/src/main/resources/mail/mail-verification.vm
deleted file mode 100644
index 340895812ca..00000000000
--- a/controller-server/src/main/resources/mail/mail-verification.vm
+++ /dev/null
@@ -1,494 +0,0 @@
-<!DOCTYPE html>
-<html
- xmlns="http://www.w3.org/1999/xhtml"
- xmlns:v="urn:schemas-microsoft-com:vml"
- xmlns:o="urn:schemas-microsoft-com:office:office"
->
- <head>
- <title></title>
- <!--[if !mso]><!-->
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
- <!--<![endif]-->
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <style type="text/css">
- #outlook a {
- padding: 0;
- }
-
- body {
- margin: 0;
- padding: 0;
- -webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
- }
-
- table,
- td {
- border-collapse: collapse;
- mso-table-lspace: 0pt;
- mso-table-rspace: 0pt;
- }
-
- img {
- border: 0;
- height: auto;
- line-height: 100%;
- outline: none;
- text-decoration: none;
- -ms-interpolation-mode: bicubic;
- }
-
- p {
- display: block;
- margin: 13px 0;
- }
- </style>
- <!--[if mso]>
- <noscript>
- <xml>
- <o:OfficeDocumentSettings>
- <o:AllowPNG />
- <o:PixelsPerInch>96</o:PixelsPerInch>
- </o:OfficeDocumentSettings>
- </xml>
- </noscript>
- <![endif]-->
- <!--[if lte mso 11]>
- <style type="text/css">
- .mj-outlook-group-fix {
- width: 100% !important;
- }
- </style>
- <![endif]-->
- <!--[if !mso]><!-->
- <link
- href="https://fonts.googleapis.com/css?family=Open Sans"
- rel="stylesheet"
- type="text/css"
- />
- <style type="text/css">
- @import url(https://fonts.googleapis.com/css?family=Open Sans);
- </style>
- <!--<![endif]-->
- <style type="text/css">
- @media only screen and (min-width: 480px) {
- .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- }
- </style>
- <style media="screen and (min-width:480px)">
- .moz-text-html .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- [owa] .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- @media only screen and (max-width: 480px) {
- table.mj-full-width-mobile {
- width: 100% !important;
- }
-
- td.mj-full-width-mobile {
- width: auto !important;
- }
- }
- </style>
- </head>
-
- <body style="word-spacing: normal; background-color: #f8f8f8">
- <div style="background-color: #f8f8f8">
- <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0;
- padding-bottom: 0px;
- padding-left: 0px;
- padding-right: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-right: 0px;
- padding-bottom: 40px;
- padding-left: 0px;
- word-break: break-word;
- "
- >
- <p
- style="
- border-top: solid 7px #3b9fde;
- font-size: 1px;
- margin: 0px auto;
- width: 100%;
- "
- ></p>
- <!--[if mso | IE
- ]><table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- style="
- border-top: solid 7px #3b9fde;
- font-size: 1px;
- margin: 0px auto;
- width: 600px;
- "
- role="presentation"
- width="600px"
- >
- <tr>
- <td style="height: 0; line-height: 0">
- &nbsp;
- </td>
- </tr>
- </table><!
- [endif]-->
- </td>
- </tr>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="
- border-collapse: collapse;
- border-spacing: 0px;
- "
- >
- <tbody>
- <tr>
- <td style="width: 110px">
- <img
- alt=""
- height="auto"
- src="https://data.vespa.oath.cloud/assets/vespa-icon.png"
- style="
- border: none;
- display: block;
- outline: none;
- text-decoration: none;
- height: auto;
- width: 100%;
- font-size: 13px;
- "
- width="110"
- />
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 70px;
- padding-top: 30px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <h1
- style="
- text-align: center;
- color: #000000;
- line-height: 32px;
- "
- >
- Verify your email address
- </h1>
- </div>
- </td>
- </tr>
- <tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <p style="margin: 10px 0; text-align: center">
- You have entered the email address <b>$esc.html($email)</b> in
- Vespa Cloud.&nbsp;
- </p>
- <p style="margin: 10px 0; text-align: center">
- Please verify this email address by clicking the
- button below.
- </p>
- </div>
- </td>
- </tr>
- <tr>
- <td
- align="center"
- vertical-align="middle"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 20px;
- padding-bottom: 20px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="border-collapse: separate; line-height: 100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- bgcolor="#3B9FDE"
- role="presentation"
- style="
- border: none;
- border-radius: 100px;
- cursor: auto;
- mso-padding-alt: 15px 25px 15px 25px;
- background: #3b9fde;
- "
- valign="middle"
- >
- <a
- href="$verifyLink"
- style="
- display: inline-block;
- background: #3b9fde;
- color: #ffffff;
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- font-weight: normal;
- line-height: 120%;
- margin: 0;
- text-decoration: none;
- text-transform: none;
- padding: 15px 25px 15px 25px;
- mso-padding-alt: 0px;
- border-radius: 100px;
- "
- target="_blank"
- ><b style="font-weight: 700"
- ><b style="font-weight: 700"
- >Verify your email</b
- ></b
- ></a
- >
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- <tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <p style="margin: 10px 0; text-align: center">
- Or copy and paste this link into your browser
- </p>
- <p style="margin: 10px 0; text-align: center">
- <a
- target="_blank"
- rel="noopener noreferrer"
- href="$verifyLink"
- style="color: #3b9fde"
- >$verifyLink</a
- >
- </p>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </div>
- </body>
-</html>
diff --git a/controller-server/src/main/resources/mail/mail.vm b/controller-server/src/main/resources/mail/mail.vm
deleted file mode 100644
index 1dbec781b3a..00000000000
--- a/controller-server/src/main/resources/mail/mail.vm
+++ /dev/null
@@ -1,516 +0,0 @@
-<!DOCTYPE html>
-<html
- xmlns="http://www.w3.org/1999/xhtml"
- xmlns:v="urn:schemas-microsoft-com:vml"
- xmlns:o="urn:schemas-microsoft-com:office:office"
->
- <head>
- <title></title>
- <!--[if !mso]><!-->
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
- <!--<![endif]-->
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <style type="text/css">
- #outlook a {
- padding: 0;
- }
-
- body {
- margin: 0;
- padding: 0;
- -webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
- }
-
- table,
- td {
- border-collapse: collapse;
- mso-table-lspace: 0pt;
- mso-table-rspace: 0pt;
- }
-
- img {
- border: 0;
- height: auto;
- line-height: 100%;
- outline: none;
- text-decoration: none;
- -ms-interpolation-mode: bicubic;
- }
-
- p {
- display: block;
- margin: 13px 0;
- }
- </style>
- <!--[if mso]>
- <noscript>
- <xml>
- <o:OfficeDocumentSettings>
- <o:AllowPNG />
- <o:PixelsPerInch>96</o:PixelsPerInch>
- </o:OfficeDocumentSettings>
- </xml>
- </noscript>
- <![endif]-->
- <!--[if lte mso 11]>
- <style type="text/css">
- .mj-outlook-group-fix {
- width: 100% !important;
- }
- </style>
- <![endif]-->
- <!--[if !mso]><!-->
- <link
- href="https://fonts.googleapis.com/css?family=Open Sans"
- rel="stylesheet"
- type="text/css"
- />
- <style type="text/css">
- @import url(https://fonts.googleapis.com/css?family=Open Sans);
- </style>
- <!--<![endif]-->
- <style type="text/css">
- @media only screen and (min-width: 480px) {
- .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- }
- </style>
- <style media="screen and (min-width:480px)">
- .moz-text-html .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- [owa] .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- @media only screen and (max-width: 480px) {
- table.mj-full-width-mobile {
- width: 100% !important;
- }
-
- td.mj-full-width-mobile {
- width: auto !important;
- }
- }
- </style>
- </head>
-
- <body style="word-spacing: normal; background-color: #f2f7fa">
- <div style="background-color: #f2f7fa">
- <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 0px 0px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <br />
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0;
- padding-bottom: 0px;
- padding-left: 0px;
- padding-right: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-right: 0px;
- padding-bottom: 40px;
- padding-left: 0px;
- word-break: break-word;
- "
- >
- <p
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 100%;
- "
- ></p>
- <!--[if mso | IE
- ]><table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 600px;
- "
- role="presentation"
- width="600px"
- >
- <tr>
- <td style="height: 0; line-height: 0">
- &nbsp;
- </td>
- </tr>
- </table><!
- [endif]-->
- </td>
- </tr>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="
- border-collapse: collapse;
- border-spacing: 0px;
- "
- >
- <tbody>
- <tr>
- <td style="width: 121px">
- <img
- alt=""
- height="auto"
- src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
- style="
- border: none;
- display: block;
- outline: none;
- text-decoration: none;
- height: auto;
- width: 100%;
- font-size: 13px;
- "
- width="121"
- />
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 70px;
- padding-top: 30px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
-
- #parse($mailBodyTemplate)
-
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 20px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 0px 20px 0px 20px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: center;
- color: #797e82;
- "
- >
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="$privacyPolicyLink"
- ><span style="color: #005a8e"
- >Yahoo Privacy Policy</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="$termsOfServiceLink"
- ><span style="color: #005a8e"
- >Terms of Service</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="$supportLink"
- ><span style="color: #005a8e">Support</span></a
- >
- </p>
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: inherit; text-decoration: none"
- href="$accountNotificationLink"
- >Click
- <span style="color: #005a8e"><u>here</u></span>
- to manage your notifications setting.</a
- ><br />
- </p>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </div>
- </body>
-</html>
diff --git a/controller-server/src/main/resources/mail/notification-message.vm b/controller-server/src/main/resources/mail/notification-message.vm
deleted file mode 100644
index 29673d38420..00000000000
--- a/controller-server/src/main/resources/mail/notification-message.vm
+++ /dev/null
@@ -1,6 +0,0 @@
-<p>
- $esc.html($notificationHeader):
-</p>
-#foreach( $i in $notificationItems )
-<p>$i</p>
-#end
diff --git a/controller-server/src/main/resources/mail/trial-expired.vm b/controller-server/src/main/resources/mail/trial-expired.vm
deleted file mode 100644
index 02d2aacd117..00000000000
--- a/controller-server/src/main/resources/mail/trial-expired.vm
+++ /dev/null
@@ -1,3 +0,0 @@
-<p>
- Your Vespa Cloud trial has expired. Please reach out to us if you have any questions or feedback.
-</p> \ No newline at end of file
diff --git a/controller-server/src/main/resources/mail/trial-expires-immediately.vm b/controller-server/src/main/resources/mail/trial-expires-immediately.vm
deleted file mode 100644
index 79604cae2e5..00000000000
--- a/controller-server/src/main/resources/mail/trial-expires-immediately.vm
+++ /dev/null
@@ -1,3 +0,0 @@
-<p>
- Your Vespa Cloud trial expires tomorrow. Please reach out to us if you have any questions or feedback.
-</p> \ No newline at end of file
diff --git a/controller-server/src/main/resources/mail/trial-midway-checkin.vm b/controller-server/src/main/resources/mail/trial-midway-checkin.vm
deleted file mode 100644
index c29c2763ef1..00000000000
--- a/controller-server/src/main/resources/mail/trial-midway-checkin.vm
+++ /dev/null
@@ -1,3 +0,0 @@
-<p>
- How is your Vespa Cloud trial going? Please reach out to us if you have any questions or feedback.
-</p> \ No newline at end of file
diff --git a/controller-server/src/main/resources/mail/trial-signed-up.vm b/controller-server/src/main/resources/mail/trial-signed-up.vm
deleted file mode 100644
index 20f1867b7bc..00000000000
--- a/controller-server/src/main/resources/mail/trial-signed-up.vm
+++ /dev/null
@@ -1,3 +0,0 @@
-<p>
- Welcome to Vespa Cloud! We hope you will enjoy your trial. Please reach out to us if you have any questions or feedback.
-</p> \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
deleted file mode 100644
index 345c880eaea..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java
+++ /dev/null
@@ -1,1584 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.google.common.collect.Sets;
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.AthenzService;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.path.Path;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeploymentData;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeploymentEndpoints;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistryMock;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerEndpoint;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.LatencyAliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedAliasTarget;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.certificate.AssignedCertificate;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.deployment.JobController;
-import com.yahoo.vespa.hosted.controller.deployment.Submission;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.notification.Notification;
-import com.yahoo.vespa.hosted.controller.notification.Notification.Level;
-import com.yahoo.vespa.hosted.controller.notification.Notification.Type;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationId;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationLock;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-import org.junit.jupiter.api.Test;
-
-import java.io.InputStream;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-import static com.yahoo.config.provision.SystemName.main;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.devAwsUsEast2a;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.devUsEast1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionAwsUsEast1a;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsEast3;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.stagingTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.systemTest;
-import static java.util.Comparator.comparing;
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author bratseth
- * @author mpolden
- */
-public class ControllerTest {
-
- private final DeploymentTester tester = new DeploymentTester();
-
- @Test
- void testDeployment() {
- // Setup system
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .explicitEnvironment(Environment.dev, Environment.perf)
- .region("us-west-1")
- .region("us-east-3")
- .build();
-
- // staging job - succeeding
- Version version1 = tester.configServer().initialVersion();
- var context = tester.newDeploymentContext();
- context.submit(applicationPackage);
- RevisionId id = RevisionId.forProduction(1);
- Version compileVersion = new Version("6.1");
- assertEquals(new ApplicationVersion(id, Optional.of(DeploymentContext.defaultSourceRevision), Optional.of("a@b"), Optional.of(compileVersion), Optional.empty(), Optional.of(Instant.ofEpochSecond(1)), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), true, false, Optional.empty(), Optional.empty(), 0),
- context.application().revisions().get(context.instance().change().revision().get()),
- "Application version is known from completion of initial job");
- context.runJob(systemTest);
- context.runJob(stagingTest);
-
- RevisionId applicationVersion = context.instance().change().revision().get();
- assertTrue(applicationVersion.isProduction(), "Application version has been set during deployment");
-
- tester.triggerJobs();
- // Causes first deployment job to be triggered
- tester.clock().advance(Duration.ofSeconds(1));
-
- // production job (failing) after deployment
- context.timeOutUpgrade(productionUsWest1);
- assertEquals(4, context.instanceJobs().size());
- tester.triggerJobs();
-
- // Simulate restart
- tester.controllerTester().createNewController();
-
- assertNotNull(tester.controller().tenants().get(TenantName.from("tenant1")));
- assertNotNull(tester.controller().applications().requireInstance(context.instanceId()));
-
- // system and staging test job - succeeding
- context.submit(applicationPackage);
- context.runJob(systemTest);
- context.runJob(stagingTest);
-
- // production job succeeding now
- context.triggerJobs().jobAborted(productionUsWest1);
- context.runJob(productionUsWest1);
-
- // causes triggering of next production job
- tester.triggerJobs();
- context.runJob(productionUsEast3);
-
- assertEquals(4, context.instanceJobs().size());
-
- // Instance with uppercase characters is not allowed.
- applicationPackage = new ApplicationPackageBuilder()
- .instances("hellO")
- .build();
- try {
- context.submit(applicationPackage);
- fail("Expected exception due to illegal deployment spec.");
- }
- catch (IllegalArgumentException e) {
- assertEquals("Invalid id 'hellO'. Tenant, application and instance names must start with a letter, may contain no more than 20 characters, and may only contain lowercase letters, digits or dashes, but no double-dashes.", e.getMessage());
- }
-
- // Production zone for which there is no JobType is not allowed.
- applicationPackage = new ApplicationPackageBuilder()
- .region("deep-space-9")
- .build();
- try {
- context.submit(applicationPackage);
- fail("Expected exception due to illegal deployment spec.");
- }
- catch (IllegalArgumentException e) {
- assertEquals("Zone prod.deep-space-9 in deployment spec was not found in this system!", e.getMessage());
- }
-
- // prod zone removal is not allowed
- applicationPackage = new ApplicationPackageBuilder()
- .region("us-east-3")
- .build();
- try {
- assertTrue(context.instance().deployments().containsKey(ZoneId.from("prod", "us-west-1")));
- context.submit(applicationPackage);
- fail("Expected exception due to illegal production deployment removal");
- }
- catch (IllegalArgumentException e) {
- assertEquals("deployment-removal: application instance 'tenant.application.default' is deployed in us-west-1, " +
- "but this instance and region combination is removed from deployment.xml. " +
- ValidationOverrides.toAllowMessage(ValidationId.deploymentRemoval),
- e.getMessage());
- }
- assertNotNull(context.instance().deployments().get(productionUsWest1.zone()),
- "Zone was not removed");
-
- // prod zone removal is allowed with override
- applicationPackage = new ApplicationPackageBuilder()
- .allow(ValidationId.deploymentRemoval)
- .upgradePolicy("default")
- .region("us-east-3")
- .build();
- context.submit(applicationPackage);
- assertNull(context.instance().deployments().get(productionUsWest1.zone()),
- "Zone was removed");
- assertNull(context.instanceJobs().get(productionUsWest1), "Deployment job was removed");
-
- // Submission has stored application meta.
- assertNotNull(tester.controllerTester().serviceRegistry().applicationStore()
- .getMeta(context.instanceId())
- .get(tester.clock().instant()));
-
- // Meta data tombstone placed on delete
- tester.clock().advance(Duration.ofSeconds(1));
- context.submit(ApplicationPackage.deploymentRemoval());
- tester.clock().advance(Duration.ofSeconds(1));
- context.submit(ApplicationPackage.deploymentRemoval());
- tester.applications().deleteApplication(context.application().id(),
- tester.controllerTester().credentialsFor(context.instanceId().tenant()));
- assertArrayEquals(new byte[0],
- tester.controllerTester().serviceRegistry().applicationStore()
- .getMeta(context.instanceId())
- .get(tester.clock().instant()));
-
- assertNull(tester.controllerTester().serviceRegistry().applicationStore()
- .getMeta(context.deploymentIdIn(productionUsWest1.zone())));
- }
-
- @Test
- void testPackagePruning() {
- DeploymentContext app = tester.newDeploymentContext().submit().deploy();
- RevisionId revision1 = app.lastSubmission().get();
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision1.number()));
-
- app.submit().deploy();
- RevisionId revision2 = app.lastSubmission().get();
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision1.number()));
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision2.number()));
-
- // Revision 1 is marked as obsolete now
- app.submit().deploy();
- RevisionId revision3 = app.lastSubmission().get();
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision1.number()));
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision2.number()));
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision3.number()));
-
- // Time advances, and revision 2 is marked as obsolete now
- tester.clock().advance(JobController.obsoletePackageExpiry);
- app.submit().deploy();
- RevisionId revision4 = app.lastSubmission().get();
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision1.number()));
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision2.number()));
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision3.number()));
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision4.number()));
-
- // Time advances, and revision is now old enough to be pruned
- tester.clock().advance(Duration.ofMillis(1));
- app.submit().deploy();
- RevisionId revision5 = app.lastSubmission().get();
- assertFalse(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision1.number()));
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision2.number()));
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision3.number()));
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision4.number()));
- assertTrue(tester.controllerTester().serviceRegistry().applicationStore()
- .hasBuild(app.instanceId().tenant(), app.instanceId().application(), revision5.number()));
- }
-
- @Test
- void testGlobalRotationStatus() {
- var context = tester.newDeploymentContext();
- var zone1 = ZoneId.from("prod", "us-west-1");
- var zone2 = ZoneId.from("prod", "us-east-3");
- var applicationPackage = new ApplicationPackageBuilder()
- .region(zone1.region())
- .region(zone2.region())
- .endpoint("default", "default", zone1.region().value(), zone2.region().value())
- .build();
- context.submit(applicationPackage).deploy();
-
- // Check initial rotation status
- var deployment1 = context.deploymentIdIn(zone1);
- DeploymentRoutingContext routingContext = tester.controller().routing().of(deployment1);
- RoutingStatus status1 = routingContext.routingStatus();
- assertEquals(RoutingStatus.Value.in, status1.value());
-
- // Set the deployment out of service in the global rotation
- routingContext.setRoutingStatus(RoutingStatus.Value.out, RoutingStatus.Agent.operator);
- RoutingStatus status2 = routingContext.routingStatus();
- assertEquals(RoutingStatus.Value.out, status2.value());
-
- // Other deployment remains in
- RoutingStatus status3 = tester.controller().routing().of(context.deploymentIdIn(zone2)).routingStatus();
- assertEquals(RoutingStatus.Value.in, status3.value());
- }
-
- @Test
- void testDnsUpdatesForGlobalEndpoint() {
- var betaContext = tester.newDeploymentContext("tenant1", "app1", "beta");
- var defaultContext = tester.newDeploymentContext("tenant1", "app1", "default");
-
- ZoneId usWest = ZoneId.from("prod.us-west-1");
- ZoneId usCentral = ZoneId.from("prod.us-central-1");
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .instances("beta,default")
- .endpoint("default", "foo")
- .region(usWest.region())
- .region(usCentral.region()) // Two deployments should result in each DNS alias being registered once
- .build();
- tester.controllerTester().zoneRegistry().setRoutingMethod(List.of(ZoneApiMock.from(usWest), ZoneApiMock.from(usCentral)),
- RoutingMethod.sharedLayer4);
- betaContext.submit(applicationPackage).deploy();
-
- { // Expected rotation names are passed to beta instance deployments
- Collection<Deployment> betaDeployments = betaContext.instance().deployments().values();
- assertFalse(betaDeployments.isEmpty());
- Set<ContainerEndpoint> containerEndpoints = Set.of(new ContainerEndpoint("foo",
- "global",
- List.of("beta.app1.tenant1.global.vespa.oath.cloud",
- "rotation-id-01"),
- OptionalInt.empty(),
- RoutingMethod.sharedLayer4,
- AuthMethod.mtls));
-
- for (Deployment deployment : betaDeployments) {
- assertEquals(containerEndpoints,
- tester.configServer().containerEndpoints()
- .get(betaContext.deploymentIdIn(deployment.zone())));
- }
- betaContext.flushDnsUpdates();
- }
-
- { // Expected rotation names are passed to default instance deployments
- Collection<Deployment> defaultDeployments = defaultContext.instance().deployments().values();
- assertFalse(defaultDeployments.isEmpty());
- Set<ContainerEndpoint> containerEndpoints = Set.of(new ContainerEndpoint("foo",
- "global",
- List.of("app1.tenant1.global.vespa.oath.cloud",
- "rotation-id-02"),
- OptionalInt.empty(),
- RoutingMethod.sharedLayer4,
- AuthMethod.mtls));
- for (Deployment deployment : defaultDeployments) {
- assertEquals(containerEndpoints,
- tester.configServer().containerEndpoints().get(defaultContext.deploymentIdIn(deployment.zone())));
- }
- defaultContext.flushDnsUpdates();
- }
-
- Map<String, String> rotationCnames = Map.of("beta.app1.tenant1.global.vespa.oath.cloud", "rotation-fqdn-01.",
- "app1.tenant1.global.vespa.oath.cloud", "rotation-fqdn-02.");
- rotationCnames.forEach((cname, data) -> {
- var record = tester.controllerTester().findCname(cname);
- assertTrue(record.isPresent());
- assertEquals(cname, record.get().name().asString());
- assertEquals(data, record.get().data().asString());
- });
-
- Map<ApplicationId, Set<String>> globalDnsNamesByInstance = Map.of(betaContext.instanceId(), Set.of("beta.app1.tenant1.global.vespa.oath.cloud"),
- defaultContext.instanceId(), Set.of("app1.tenant1.global.vespa.oath.cloud"));
-
- globalDnsNamesByInstance.forEach((instance, dnsNames) -> {
- Set<String> actualDnsNames = tester.controller().routing().readDeclaredEndpointsOf(instance)
- .scope(Endpoint.Scope.global)
- .asList().stream()
- .map(Endpoint::dnsName)
- .collect(Collectors.toSet());
- assertEquals(dnsNames, actualDnsNames, "Global DNS names for " + instance);
- });
- }
-
- @Test
- void testDnsUpdatesForGlobalEndpointLegacySyntax() {
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "foo")
- .region("us-west-1")
- .region("us-central-1") // Two deployments should result in each DNS alias being registered once
- .build();
- context.submit(applicationPackage).deploy();
-
- Collection<Deployment> deployments = context.instance().deployments().values();
- assertFalse(deployments.isEmpty());
- for (Deployment deployment : deployments) {
- assertEquals(Set.of("rotation-id-01",
- "app1.tenant1.global.vespa.oath.cloud"),
- tester.configServer().containerEndpointNames(context.deploymentIdIn(deployment.zone())),
- "Rotation names are passed to config server in " + deployment.zone());
- }
- context.flushDnsUpdates();
- assertEquals(1, tester.controllerTester().nameService().records().size());
-
- Optional<Record> record = tester.controllerTester().findCname("app1.tenant1.global.vespa.oath.cloud");
- assertTrue(record.isPresent());
- assertEquals("app1.tenant1.global.vespa.oath.cloud", record.get().name().asString());
- assertEquals("rotation-fqdn-01.", record.get().data().asString());
-
- List<String> globalDnsNames = tester.controller().routing().readDeclaredEndpointsOf(context.instanceId())
- .scope(Endpoint.Scope.global)
- .sortedBy(comparing(Endpoint::dnsName))
- .mapToList(Endpoint::dnsName);
- assertEquals(List.of("app1.tenant1.global.vespa.oath.cloud"),
- globalDnsNames);
- }
-
- @Test
- void testDnsUpdatesForMultipleGlobalEndpoints() {
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .endpoint("foobar", "qrs", "us-west-1", "us-central-1") // Rotation 01
- .endpoint("default", "qrs", "us-west-1", "us-central-1") // Rotation 02
- .endpoint("all", "qrs") // Rotation 03
- .endpoint("west", "qrs", "us-west-1") // Rotation 04
- .region("us-west-1")
- .region("us-central-1")
- .build();
- context.submit(applicationPackage).deploy();
-
- Collection<Deployment> deployments = context.instance().deployments().values();
- assertFalse(deployments.isEmpty());
-
- var notWest = Set.of(
- "rotation-id-01", "foobar.app1.tenant1.global.vespa.oath.cloud",
- "rotation-id-02", "app1.tenant1.global.vespa.oath.cloud",
- "rotation-id-03", "all.app1.tenant1.global.vespa.oath.cloud"
- );
- var west = Sets.union(notWest, Set.of("rotation-id-04", "west.app1.tenant1.global.vespa.oath.cloud"));
-
- for (Deployment deployment : deployments) {
- assertEquals(ZoneId.from("prod.us-west-1").equals(deployment.zone()) ? west : notWest,
- tester.configServer().containerEndpointNames(context.deploymentIdIn(deployment.zone())),
- "Rotation names are passed to config server in " + deployment.zone());
- }
- context.flushDnsUpdates();
-
- assertEquals(4, tester.controllerTester().nameService().records().size());
-
- var record1 = tester.controllerTester().findCname("app1.tenant1.global.vespa.oath.cloud");
- assertTrue(record1.isPresent());
- assertEquals("app1.tenant1.global.vespa.oath.cloud", record1.get().name().asString());
- assertEquals("rotation-fqdn-02.", record1.get().data().asString());
-
- var record2 = tester.controllerTester().findCname("foobar.app1.tenant1.global.vespa.oath.cloud");
- assertTrue(record2.isPresent());
- assertEquals("foobar.app1.tenant1.global.vespa.oath.cloud", record2.get().name().asString());
- assertEquals("rotation-fqdn-01.", record2.get().data().asString());
-
- var record3 = tester.controllerTester().findCname("all.app1.tenant1.global.vespa.oath.cloud");
- assertTrue(record3.isPresent());
- assertEquals("all.app1.tenant1.global.vespa.oath.cloud", record3.get().name().asString());
- assertEquals("rotation-fqdn-03.", record3.get().data().asString());
-
- var record4 = tester.controllerTester().findCname("west.app1.tenant1.global.vespa.oath.cloud");
- assertTrue(record4.isPresent());
- assertEquals("west.app1.tenant1.global.vespa.oath.cloud", record4.get().name().asString());
- assertEquals("rotation-fqdn-04.", record4.get().data().asString());
- }
-
- @Test
- void testDnsUpdatesForGlobalEndpointChanges() {
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- var west = ZoneId.from("prod", "us-west-1");
- var central = ZoneId.from("prod", "us-central-1");
- var east = ZoneId.from("prod", "us-east-3");
-
- // Application is deployed with endpoint pointing to 2/3 zones
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "qrs", west.region().value(), central.region().value())
- .region(west.region().value())
- .region(central.region().value())
- .region(east.region().value())
- .build();
- context.submit(applicationPackage).deploy();
-
- for (var zone : List.of(west, central)) {
- assertEquals(
- Set.of("rotation-id-01", "app1.tenant1.global.vespa.oath.cloud"),
- tester.configServer().containerEndpointNames(context.deploymentIdIn(zone))
- ,
- "Zone " + zone + " is a member of global endpoint");
- }
-
- // Application is deployed with an additional endpoint
- ApplicationPackage applicationPackage2 = new ApplicationPackageBuilder()
- .endpoint("default", "qrs", west.region().value(), central.region().value())
- .endpoint("east", "qrs", east.region().value())
- .region(west.region().value())
- .region(central.region().value())
- .region(east.region().value())
- .build();
- context.submit(applicationPackage2).deploy();
-
- for (var zone : List.of(west, central)) {
- assertEquals(
- Set.of("rotation-id-01", "app1.tenant1.global.vespa.oath.cloud"),
- tester.configServer().containerEndpointNames(context.deploymentIdIn(zone))
- ,
- "Zone " + zone + " is a member of global endpoint");
- }
- assertEquals(
- Set.of("rotation-id-02", "east.app1.tenant1.global.vespa.oath.cloud"),
- tester.configServer().containerEndpointNames(context.deploymentIdIn(east))
- ,
- "Zone " + east + " is a member of global endpoint");
-
- // Application is deployed with default endpoint pointing to 3/3 zones
- ApplicationPackage applicationPackage3 = new ApplicationPackageBuilder()
- .endpoint("default", "qrs", west.region().value(), central.region().value(), east.region().value())
- .endpoint("east", "qrs", east.region().value())
- .region(west.region().value())
- .region(central.region().value())
- .region(east.region().value())
- .build();
- context.submit(applicationPackage3).deploy();
- for (var zone : List.of(west, central, east)) {
- assertEquals(
- zone.equals(east)
- ? Set.of("rotation-id-01", "app1.tenant1.global.vespa.oath.cloud",
- "rotation-id-02", "east.app1.tenant1.global.vespa.oath.cloud")
- : Set.of("rotation-id-01", "app1.tenant1.global.vespa.oath.cloud"),
- tester.configServer().containerEndpointNames(context.deploymentIdIn(zone))
- ,
- "Zone " + zone + " is a member of global endpoint");
- }
-
- // Region is removed from an endpoint without override
- ApplicationPackage applicationPackage4 = new ApplicationPackageBuilder()
- .endpoint("default", "qrs", west.region().value(), central.region().value())
- .endpoint("east", "qrs", east.region().value())
- .region(west.region().value())
- .region(central.region().value())
- .region(east.region().value())
- .build();
- try {
- context.submit(applicationPackage4);
- fail("Expected exception");
- } catch (IllegalArgumentException e) {
- assertEquals("global-endpoint-change: application 'tenant1.app1' has endpoints " +
- "[endpoint 'default' (cluster qrs) -> us-central-1, us-east-3, us-west-1, endpoint 'east' (cluster qrs) -> us-east-3], " +
- "but does not include all of these in deployment.xml. Deploying given deployment.xml " +
- "will remove [endpoint 'default' (cluster qrs) -> us-central-1, us-east-3, us-west-1] " +
- "and add [endpoint 'default' (cluster qrs) -> us-central-1, us-west-1]. " +
- ValidationOverrides.toAllowMessage(ValidationId.globalEndpointChange), e.getMessage());
- }
-
- // Entire endpoint is removed without override
- ApplicationPackage applicationPackage5 = new ApplicationPackageBuilder()
- .endpoint("east", "qrs", east.region().value())
- .region(west.region().value())
- .region(central.region().value())
- .region(east.region().value())
- .build();
- try {
- context.submit(applicationPackage5);
- fail("Expected exception");
- } catch (IllegalArgumentException e) {
- assertEquals("global-endpoint-change: application 'tenant1.app1' has endpoints " +
- "[endpoint 'default' (cluster qrs) -> us-central-1, us-east-3, us-west-1, endpoint 'east' (cluster qrs) -> us-east-3], " +
- "but does not include all of these in deployment.xml. Deploying given deployment.xml " +
- "will remove [endpoint 'default' (cluster qrs) -> us-central-1, us-east-3, us-west-1]. " +
- ValidationOverrides.toAllowMessage(ValidationId.globalEndpointChange), e.getMessage());
- }
-
- // ... override is added
- ApplicationPackage applicationPackage6 = new ApplicationPackageBuilder()
- .endpoint("east", "qrs", east.region().value())
- .region(west.region().value())
- .region(central.region().value())
- .region(east.region().value())
- .allow(ValidationId.globalEndpointChange)
- .build();
- context.submit(applicationPackage6);
- }
-
- @Test
- void testUnassignRotations() {
- var context = tester.newDeploymentContext();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "qrs", "us-west-1", "us-central-1")
- .region("us-west-1")
- .region("us-central-1")
- .build();
- context.submit(applicationPackage).deploy();
-
- ApplicationPackage applicationPackage2 = new ApplicationPackageBuilder()
- .region("us-west-1")
- .region("us-central-1")
- .allow(ValidationId.globalEndpointChange)
- .build();
-
- context.submit(applicationPackage2).deploy();
-
- assertEquals(List.of(), context.instance().rotations());
-
- assertEquals(
- Set.of(),
- tester.configServer().containerEndpoints().get(context.deploymentIdIn(ZoneId.from("prod", "us-west-1")))
- );
- }
-
- @Test
- void testDnsUpdatesWithChangeInRotationAssignment() {
- // Application 1 is deployed and deleted
- String dnsName1 = "app1.tenant1.global.vespa.oath.cloud";
- {
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "foo")
- .region("us-west-1")
- .region("us-central-1") // Two deployments should result in each DNS alias being registered once
- .build();
-
- context.submit(applicationPackage).deploy();
- assertEquals(1, tester.controllerTester().nameService().records().size());
- {
- Optional<Record> record = tester.controllerTester().findCname(dnsName1);
- assertTrue(record.isPresent());
- assertEquals(dnsName1, record.get().name().asString());
- assertEquals("rotation-fqdn-01.", record.get().data().asString());
- }
-
- // Application is deleted and rotation is unassigned
- applicationPackage = new ApplicationPackageBuilder()
- .allow(ValidationId.deploymentRemoval)
- .allow(ValidationId.globalEndpointChange)
- .build();
- context.submit(applicationPackage);
- tester.applications().deleteApplication(context.application().id(),
- tester.controllerTester().credentialsFor(context.application().id().tenant()));
- try (RotationLock lock = tester.controller().routing().rotations().lock()) {
- assertTrue(tester.controller().routing().rotations().availableRotations(lock)
- .containsKey(new RotationId("rotation-id-01")),
- "Rotation is unassigned");
- }
- context.flushDnsUpdates();
-
- // Record is removed
- Optional<Record> record = tester.controllerTester().findCname(dnsName1);
- assertTrue(record.isEmpty(), dnsName1 + " is removed");
- }
-
- // Application 2 is deployed and assigned same rotation as application 1 had before deletion
- String dnsName2 = "app2.tenant2.global.vespa.oath.cloud";
- {
- var context = tester.newDeploymentContext("tenant2", "app2", "default");
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "foo")
- .region("us-west-1")
- .region("us-central-1")
- .build();
- context.submit(applicationPackage).deploy();
- assertEquals(1, tester.controllerTester().nameService().records().size());
-
- var record = tester.controllerTester().findCname(dnsName2);
- assertTrue(record.isPresent());
- assertEquals(dnsName2, record.get().name().asString());
- assertEquals("rotation-fqdn-01.", record.get().data().asString());
- }
-
- // Application 1 is recreated, deployed and assigned a new rotation
- {
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "foo")
- .region("us-west-1")
- .region("us-central-1")
- .build();
- context.submit(applicationPackage).deploy();
- assertEquals("rotation-id-02", context.instance().rotations().get(0).rotationId().asString());
-
- // DNS records are created for the newly assigned rotation
- assertEquals(2, tester.controllerTester().nameService().records().size());
-
- var record1 = tester.controllerTester().findCname(dnsName1);
- assertTrue(record1.isPresent());
- assertEquals("rotation-fqdn-02.", record1.get().data().asString());
-
- var record2 = tester.controllerTester().findCname(dnsName2);
- assertTrue(record2.isPresent());
- assertEquals("rotation-fqdn-01.", record2.get().data().asString());
- }
-
- }
-
- @Test
- void testDnsUpdatesForApplicationEndpoint() {
- ApplicationId beta = ApplicationId.from("tenant1", "app1", "beta");
- ApplicationId main = ApplicationId.from("tenant1", "app1", "main");
- var context = tester.newDeploymentContext(beta);
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .instances("beta,main")
- .region("us-east-3")
- .region("us-west-1")
- .region("aws-us-east-1a")
- .region("aws-us-east-1b")
- .applicationEndpoint("a", "default",
- Map.of("aws-us-east-1a", Map.of(beta.instance(), 2,
- main.instance(), 8),
- "aws-us-east-1b", Map.of(main.instance(), 1)))
- .applicationEndpoint("b", "default", "aws-us-east-1a",
- Map.of(beta.instance(), 1,
- main.instance(), 1))
- .applicationEndpoint("c", "default", "aws-us-east-1b",
- Map.of(beta.instance(), 4))
- .applicationEndpoint("d", "default", "us-west-1",
- Map.of(main.instance(), 7,
- beta.instance(), 3))
- .applicationEndpoint("e", "default", "us-east-3",
- Map.of(main.instance(), 3))
- .build();
- context.submit(applicationPackage).deploy();
-
- ZoneId east3 = ZoneId.from("prod", "us-east-3");
- ZoneId west1 = ZoneId.from("prod", "us-west-1");
- ZoneId east1a = ZoneId.from("prod", "aws-us-east-1a");
- ZoneId east1b = ZoneId.from("prod", "aws-us-east-1b");
- // Expected container endpoints are passed to each deployment
- Map<DeploymentId, Map<List<String>, Integer>> deploymentEndpoints = Map.of(
- new DeploymentId(beta, east3), Map.of(),
- new DeploymentId(main, east3), Map.of(List.of("e.app1.tenant1.a.vespa.oath.cloud"), 3),
- new DeploymentId(beta, west1), Map.of(List.of("d.app1.tenant1.a.vespa.oath.cloud"), 3),
- new DeploymentId(main, west1), Map.of(List.of("d.app1.tenant1.a.vespa.oath.cloud"), 7),
- new DeploymentId(beta, east1a), Map.of(List.of("a.app1.tenant1.a.vespa.oath.cloud"), 2,
- List.of("b.app1.tenant1.a.vespa.oath.cloud"), 1),
- new DeploymentId(main, east1a), Map.of(List.of("a.app1.tenant1.a.vespa.oath.cloud"), 8,
- List.of("b.app1.tenant1.a.vespa.oath.cloud"), 1),
- new DeploymentId(beta, east1b), Map.of(List.of("c.app1.tenant1.a.vespa.oath.cloud"), 4),
- new DeploymentId(main, east1b), Map.of(List.of("a.app1.tenant1.a.vespa.oath.cloud"), 1)
- );
- deploymentEndpoints.forEach((deployment, endpoints) -> {
- Set<ContainerEndpoint> expected = endpoints.entrySet().stream()
- .map(kv -> new ContainerEndpoint("default", "application",
- kv.getKey(),
- OptionalInt.of(kv.getValue()),
- tester.controller().zoneRegistry().routingMethod(deployment.zoneId()),
- AuthMethod.mtls))
- .collect(Collectors.toSet());
- assertEquals(expected,
- tester.configServer().containerEndpoints().get(deployment),
- "Endpoint names for " + deployment + " are passed to config server");
- });
- context.flushDnsUpdates();
-
- // DNS records are created for each endpoint
- Set<Record> records = tester.controllerTester().nameService().records();
- assertEquals(new TreeSet<>(Set.of(new Record(Record.Type.CNAME,
- RecordName.from("beta.app1.tenant1.aws-us-east-1a.vespa.oath.cloud"),
- RecordData.from("lb-0--tenant1.app1.beta--prod.aws-us-east-1a.")),
- new Record(Record.Type.CNAME,
- RecordName.from("beta.app1.tenant1.aws-us-east-1b.vespa.oath.cloud"),
- RecordData.from("lb-0--tenant1.app1.beta--prod.aws-us-east-1b.")),
- new Record(Record.Type.CNAME,
- RecordName.from("main.app1.tenant1.aws-us-east-1a.vespa.oath.cloud"),
- RecordData.from("lb-0--tenant1.app1.main--prod.aws-us-east-1a.")),
- new Record(Record.Type.CNAME,
- RecordName.from("main.app1.tenant1.aws-us-east-1b.vespa.oath.cloud"),
- RecordData.from("lb-0--tenant1.app1.main--prod.aws-us-east-1b.")),
- new Record(Record.Type.ALIAS,
- RecordName.from("a.app1.tenant1.a.vespa.oath.cloud"),
- RecordData.from("weighted/lb-0--tenant1.app1.beta--prod.aws-us-east-1a/dns-zone-1/prod.aws-us-east-1a/2")),
- new Record(Record.Type.ALIAS,
- RecordName.from("a.app1.tenant1.a.vespa.oath.cloud"),
- RecordData.from("weighted/lb-0--tenant1.app1.main--prod.aws-us-east-1a/dns-zone-1/prod.aws-us-east-1a/8")),
- new Record(Record.Type.ALIAS,
- RecordName.from("a.app1.tenant1.a.vespa.oath.cloud"),
- RecordData.from("weighted/lb-0--tenant1.app1.main--prod.aws-us-east-1b/dns-zone-1/prod.aws-us-east-1b/1")),
- new Record(Record.Type.ALIAS,
- RecordName.from("b.app1.tenant1.a.vespa.oath.cloud"),
- RecordData.from("weighted/lb-0--tenant1.app1.beta--prod.aws-us-east-1a/dns-zone-1/prod.aws-us-east-1a/1")),
- new Record(Record.Type.ALIAS,
- RecordName.from("b.app1.tenant1.a.vespa.oath.cloud"),
- RecordData.from("weighted/lb-0--tenant1.app1.main--prod.aws-us-east-1a/dns-zone-1/prod.aws-us-east-1a/1")),
- new Record(Record.Type.ALIAS,
- RecordName.from("c.app1.tenant1.a.vespa.oath.cloud"),
- RecordData.from("weighted/lb-0--tenant1.app1.beta--prod.aws-us-east-1b/dns-zone-1/prod.aws-us-east-1b/4")),
- new Record(Record.Type.CNAME,
- RecordName.from("d.app1.tenant1.a.vespa.oath.cloud"),
- RecordData.from("vip.prod.us-west-1.")),
- new Record(Record.Type.CNAME,
- RecordName.from("e.app1.tenant1.a.vespa.oath.cloud"),
- RecordData.from("vip.prod.us-east-3.")))),
- new TreeSet<>(records));
- List<String> endpointDnsNames = tester.controller().routing().readDeclaredEndpointsOf(context.application())
- .scope(Endpoint.Scope.application)
- .sortedBy(comparing(Endpoint::dnsName))
- .mapToList(Endpoint::dnsName);
- assertEquals(List.of("a.app1.tenant1.a.vespa.oath.cloud",
- "b.app1.tenant1.a.vespa.oath.cloud",
- "c.app1.tenant1.a.vespa.oath.cloud",
- "d.app1.tenant1.a.vespa.oath.cloud",
- "e.app1.tenant1.a.vespa.oath.cloud"),
- endpointDnsNames);
- }
-
- @Test
- void testDevDeployment() {
- // A package without deployment.xml is considered valid
- ApplicationPackage applicationPackage = new ApplicationPackage(new byte[0]);
-
- // Create application
- var context = tester.newDeploymentContext();
- ZoneId zone = ZoneId.from("dev", "us-east-1");
- tester.controllerTester().zoneRegistry()
- .setRoutingMethod(ZoneApiMock.from(zone), RoutingMethod.sharedLayer4);
-
- // Deploy
- context.runJob(zone, applicationPackage);
- assertTrue(tester.configServer().application(context.instanceId(), zone).get().activated(),
- "Application deployed and activated");
- assertTrue(context.instanceJobs().isEmpty(),
- "No job status added");
- assertEquals(DeploymentSpec.empty, context.application().deploymentSpec(), "DeploymentSpec is not stored");
-
- // Verify zone supports shared layer 4 and shared routing methods
- Set<RoutingMethod> routingMethods = tester.controller().routing().readEndpointsOf(context.deploymentIdIn(zone))
- .asList()
- .stream()
- .map(Endpoint::routingMethod)
- .collect(Collectors.toSet());
- assertEquals(routingMethods, Set.of(RoutingMethod.sharedLayer4));
-
- // Deployment has stored application meta.
- assertNotNull(tester.controllerTester().serviceRegistry().applicationStore()
- .getMeta(new DeploymentId(context.instanceId(), zone))
- .get(tester.clock().instant()));
-
- // Meta data tombstone placed on delete
- tester.clock().advance(Duration.ofSeconds(1));
- tester.controller().applications().deactivate(context.instanceId(), zone);
- assertArrayEquals(new byte[0],
- tester.controllerTester().serviceRegistry().applicationStore()
- .getMeta(new DeploymentId(context.instanceId(), zone))
- .get(tester.clock().instant()));
- }
-
- @Test
- void testDevDeploymentWithIncompatibleVersions() {
- Version version1 = new Version("7");
- Version version2 = new Version("7.5");
- Version version3 = new Version("8");
- var context = tester.newDeploymentContext();
- tester.controllerTester().flagSource().withListFlag(PermanentFlags.INCOMPATIBLE_VERSIONS.id(), List.of("8"), String.class);
- tester.controllerTester().upgradeSystem(version2);
- tester.newDeploymentContext("keep", "v2", "alive").submit().deploy(); // TODO jonmv: remove
- ZoneId zone = ZoneId.from("dev", "us-east-1");
-
- context.runJob(zone, new ApplicationPackageBuilder().compileVersion(version1).build());
- assertEquals(version2, context.deployment(zone).version());
- assertEquals(Optional.of(version1), context.application().revisions().get(context.deployment(zone).revision()).compileVersion());
-
- try {
- context.runJob(zone, new ApplicationPackageBuilder().compileVersion(version1).majorVersion(8).build());
- fail("Should fail when specifying a major that does not yet exist");
- }
- catch (IllegalArgumentException e) {
- assertEquals("no platforms were found for major version 8 specified in deployment.xml", e.getMessage());
- }
-
- try {
- context.runJob(zone, new ApplicationPackageBuilder().compileVersion(version3).build());
- fail("Should fail when compiled against a version which is only compatible with not-yet-existent versions");
- }
- catch (IllegalArgumentException e) {
- assertEquals("no platforms are compatible with compile version 8", e.getMessage());
- }
-
- tester.controllerTester().upgradeSystem(version3);
- try {
- context.runJob(zone, new ApplicationPackageBuilder().compileVersion(version1).majorVersion(8).build());
- fail("Should fail when specifying a major which is incompatible with compile version");
- }
- catch (IllegalArgumentException e) {
- assertEquals("no platforms on major version 8 specified in deployment.xml are compatible with compile version 7", e.getMessage());
- }
-
- context.runJob(zone, new ApplicationPackageBuilder().compileVersion(version3).majorVersion(8).build());
- assertEquals(version3, context.deployment(zone).version());
- assertEquals(Optional.of(version3), context.application().revisions().get(context.deployment(zone).revision()).compileVersion());
-
- context.runJob(zone, new ApplicationPackageBuilder().compileVersion(version3).build());
- assertEquals(version3, context.deployment(zone).version());
- assertEquals(Optional.of(version3), context.application().revisions().get(context.deployment(zone).revision()).compileVersion());
- }
-
- @Test
- void testSuspension() {
- var context = tester.newDeploymentContext();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .region("us-east-3")
- .build();
- context.submit(applicationPackage).deploy();
-
- DeploymentId deployment1 = context.deploymentIdIn(ZoneId.from(Environment.prod, RegionName.from("us-west-1")));
- DeploymentId deployment2 = context.deploymentIdIn(ZoneId.from(Environment.prod, RegionName.from("us-east-3")));
- assertFalse(tester.configServer().isSuspended(deployment1));
- assertFalse(tester.configServer().isSuspended(deployment2));
- tester.configServer().setSuspension(deployment1, true);
- assertTrue(tester.configServer().isSuspended(deployment1));
- assertFalse(tester.configServer().isSuspended(deployment2));
- }
-
- // Application may already have been deleted, or deployment failed without response, test that deleting a
- // second time will not fail
- @Test
- void testDeletingApplicationThatHasAlreadyBeenDeleted() {
- var context = tester.newDeploymentContext();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
-
- ZoneId zone = ZoneId.from(Environment.prod, RegionName.from("us-west-1"));
- context.submit(applicationPackage).runJob(zone, applicationPackage);
- tester.controller().applications().deactivate(context.instanceId(), zone);
- tester.controller().applications().deactivate(context.instanceId(), zone);
- }
-
- @Test
- void testDeployApplicationWithWarnings() {
- var context = tester.newDeploymentContext();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
- ZoneId zone = ZoneId.from("prod", "us-west-1");
- int warnings = 3;
- tester.configServer().generateWarnings(context.deploymentIdIn(zone), warnings);
- context.submit(applicationPackage).deploy();
- assertEquals(warnings, context.deployment(zone)
- .metrics().warnings().get(DeploymentMetrics.Warning.all).intValue());
- }
-
- @Test
- void testDeploySelectivelyProvisionsCertificate() {
- Function<Instance, Optional<EndpointCertificate>> certificate = (application) -> tester.controller().curator().readAssignedCertificate(application.id()).map(AssignedCertificate::certificate);
-
- // Create app1
- var context1 = tester.newDeploymentContext("tenant1", "app1", "default");
- var prodZone = ZoneId.from("prod", "us-west-1");
- tester.controllerTester().zoneRegistry().exclusiveRoutingIn(ZoneApiMock.from(prodZone));
- var applicationPackage = new ApplicationPackageBuilder().athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"))
- .region(prodZone.region())
- .build();
- // Deploy app1 in production
- context1.submit(applicationPackage).deploy();
- var cert = certificate.apply(context1.instance());
- assertTrue(cert.isPresent(), "Provisions certificate in " + Environment.prod);
- assertEquals(List.of("*.app1.tenant1.global.vespa.oath.cloud",
- "*.app1.tenant1.us-east-1.test.vespa.oath.cloud",
- "*.app1.tenant1.us-east-3.staging.vespa.oath.cloud",
- "*.app1.tenant1.us-west-1.vespa.oath.cloud",
- "*.f5549014.a.vespa.oath.cloud",
- "*.f5549014.g.vespa.oath.cloud",
- "*.f5549014.z.vespa.oath.cloud",
- "app1.tenant1.global.vespa.oath.cloud",
- "app1.tenant1.us-east-1.test.vespa.oath.cloud",
- "app1.tenant1.us-east-3.staging.vespa.oath.cloud",
- "app1.tenant1.us-west-1.vespa.oath.cloud",
- "vznqtz7a5ygwjkbhhj7ymxvlrekgt4l6g.vespa.oath.cloud"),
- tester.controllerTester().serviceRegistry().endpointCertificateMock()
- .dnsNamesOf(cert.get().rootRequestId())
- .stream()
- .sorted()
- .toList());
-
- // Next deployment reuses certificate
- context1.submit(applicationPackage).deploy();
- assertEquals(cert, certificate.apply(context1.instance()));
-
- // Create app2
- var context2 = tester.newDeploymentContext("tenant1", "app2", "default");
- var devZone = ZoneId.from("dev", "us-east-1");
-
- // Deploy app2 in a zone with shared routing
- context2.runJob(devZone, applicationPackage);
- assertTrue(tester.configServer().application(context2.instanceId(), devZone).get().activated(),
- "Application deployed and activated");
- assertTrue(certificate.apply(context2.instance()).isPresent(), "Provisions certificate also in zone with routing layer");
- }
-
- @Test
- void testDeployWithGlobalEndpointsInMultipleClouds() {
- tester.controllerTester().zoneRegistry().setZones(
- ZoneApiMock.fromId("test.us-west-1"),
- ZoneApiMock.fromId("staging.us-west-1"),
- ZoneApiMock.fromId("prod.us-west-1"),
- ZoneApiMock.newBuilder().with(CloudName.AWS).withId("prod.aws-us-east-1").build()
- );
- var context = tester.newDeploymentContext();
- var applicationPackage = new ApplicationPackageBuilder()
- .region("aws-us-east-1")
- .region("us-west-1")
- .endpoint("default", "default") // Contains to all regions by default
- .build();
-
- try {
- context.submit(applicationPackage);
- fail("Expected exception");
- } catch (IllegalArgumentException e) {
- assertEquals("Endpoint 'default' in instance 'default' cannot contain regions in different clouds: [aws-us-east-1, us-west-1]", e.getMessage());
- }
-
- var applicationPackage2 = new ApplicationPackageBuilder()
- .region("aws-us-east-1")
- .region("us-west-1")
- .endpoint("aws", "default", "aws-us-east-1")
- .endpoint("foo", "default", "aws-us-east-1", "us-west-1")
- .build();
- try {
- context.submit(applicationPackage2);
- fail("Expected exception");
- } catch (IllegalArgumentException e) {
- assertEquals("Endpoint 'foo' in instance 'default' cannot contain regions in different clouds: [aws-us-east-1, us-west-1]", e.getMessage());
- }
- }
-
- @Test
- void testDeployWithoutSourceRevision() {
- var context = tester.newDeploymentContext();
- var applicationPackage = new ApplicationPackageBuilder()
- .upgradePolicy("default")
- .region("us-west-1")
- .build();
-
- // Submit without source revision
- context.submit(applicationPackage, Optional.empty())
- .deploy();
- assertEquals(1, context.instance().deployments().size(), "Deployed application");
- }
-
- @Test
- void testDeployWithGlobalEndpointsAndMultipleRoutingMethods() {
- var context = tester.newDeploymentContext();
- var zone1 = ZoneId.from("prod", "aws-us-east-1a");
- var zone2 = ZoneId.from("prod", "aws-us-east-1b");
- var applicationPackage = new ApplicationPackageBuilder()
- .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"))
- .endpoint("default", "default", zone1.region().value(), zone2.region().value())
- .endpoint("east", "default", zone2.region().value())
- .region(zone1.region())
- .region(zone2.region())
- .build();
-
- // Zone 1 supports sharedLayer4
- tester.controllerTester().zoneRegistry().setRoutingMethod(ZoneApiMock.from(zone1), RoutingMethod.sharedLayer4);
- // Zone 2 supports shared and exclusive
- tester.controllerTester().zoneRegistry().setRoutingMethod(ZoneApiMock.from(zone2), RoutingMethod.exclusive);
-
- context.submit(applicationPackage).deploy();
- var expectedRecords = List.of(
- // The weighted record for zone 2's region
- new Record(Record.Type.ALIAS,
- RecordName.from("application.tenant.aws-us-east-1-w.vespa.oath.cloud"),
- new WeightedAliasTarget(HostName.of("lb-0--tenant.application.default--prod.aws-us-east-1b"),
- "dns-zone-1", "prod.aws-us-east-1b", 1).pack()),
-
- // The 'east' global endpoint, pointing to the weighted record for zone 2's region
- new Record(Record.Type.ALIAS,
- RecordName.from("east.application.tenant.global.vespa.oath.cloud"),
- new LatencyAliasTarget(HostName.of("application.tenant.aws-us-east-1-w.vespa.oath.cloud"),
- "dns-zone-1", ZoneId.from("prod.aws-us-east-1b")).pack()),
-
- // The zone-scoped endpoint pointing to zone 2 with exclusive routing
- new Record(Record.Type.CNAME,
- RecordName.from("application.tenant.aws-us-east-1b.vespa.oath.cloud"),
- RecordData.from("lb-0--tenant.application.default--prod.aws-us-east-1b.")));
- assertEquals(expectedRecords, List.copyOf(tester.controllerTester().nameService().records()));
- }
-
- @Test
- void testDeploymentDirectRouting() {
- // Rotation-less system
- DeploymentTester tester = new DeploymentTester(new ControllerTester(new RotationsConfig.Builder().build(), main));
- var context = tester.newDeploymentContext();
- var zone1 = ZoneId.from("prod", "us-west-1");
- var zone2 = ZoneId.from("prod", "us-east-3");
- var zone3 = ZoneId.from("prod", "eu-west-1");
- tester.controllerTester().zoneRegistry()
- .exclusiveRoutingIn(ZoneApiMock.from(zone1), ZoneApiMock.from(zone2), ZoneApiMock.from(zone3));
- tester.controller().dataplaneTokenService().generateToken(context.application().id().tenant(), TokenId.of("token-1"), null, () -> "foo");
- tester.clock().advance(Duration.ofSeconds(1));
- tester.controller().dataplaneTokenService().generateToken(context.application().id().tenant(), TokenId.of("token-2"), null, () -> "foo");
-
- var applicationPackageBuilder = new ApplicationPackageBuilder()
- .region(zone1.region())
- .region(zone2.region())
- .region(zone3.region())
- .container("qrs", AuthMethod.mtls, AuthMethod.token)
- .container("default", AuthMethod.mtls)
- .endpoint("default", "default")
- .endpoint("foo", "qrs")
- .endpoint("us", "default", zone1.region().value(), zone2.region().value())
- .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"));
- context.submit(applicationPackageBuilder.build()).deploy();
-
- // Deployment passes container endpoints to config server
- for (var zone : List.of(zone1, zone2)) {
- assertEquals(Set.of("application.tenant.global.vespa.oath.cloud",
- "foo.application.tenant.global.vespa.oath.cloud",
- "us.application.tenant.global.vespa.oath.cloud",
- "qrs.application.tenant." + zone.region().value() + ".vespa.oath.cloud",
- "application.tenant." + zone.region().value() + ".vespa.oath.cloud"),
- tester.configServer().containerEndpointNames(context.deploymentIdIn(zone)),
- "Expected container endpoints in " + zone);
- assertEquals(Map.of(TokenId.of("token-1"), tester.clock().instant().minusSeconds(1)),
- context.deployment(zone).dataPlaneTokens());
- }
- assertEquals(Set.of("application.tenant.global.vespa.oath.cloud",
- "foo.application.tenant.global.vespa.oath.cloud",
- "qrs.application.tenant.eu-west-1.vespa.oath.cloud",
- "application.tenant.eu-west-1.vespa.oath.cloud"),
- tester.configServer().containerEndpointNames(context.deploymentIdIn(zone3)),
- "Expected container endpoints in " + zone3);
- }
-
- @Test
- void testChangeEndpointCluster() {
- var context = tester.newDeploymentContext();
- var west = ZoneId.from("prod", "us-west-1");
- var east = ZoneId.from("prod", "us-east-3");
-
- // Deploy application
- var applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "foo")
- .region(west.region().value())
- .region(east.region().value())
- .build();
- context.submit(applicationPackage).deploy();
- assertEquals(ClusterSpec.Id.from("foo"), tester.applications().requireInstance(context.instanceId())
- .rotations().get(0).clusterId());
-
- // Redeploy with endpoint cluster changed needs override
- applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "bar")
- .region(west.region().value())
- .region(east.region().value())
- .build();
- try {
- context.submit(applicationPackage).deploy();
- fail("Expected exception");
- } catch (IllegalArgumentException e) {
- assertEquals("global-endpoint-change: application 'tenant.application' has endpoints [endpoint " +
- "'default' (cluster foo) -> us-east-3, us-west-1], but does not include all of these in " +
- "deployment.xml. Deploying given deployment.xml will remove " +
- "[endpoint 'default' (cluster foo) -> us-east-3, us-west-1] and add " +
- "[endpoint 'default' (cluster bar) -> us-east-3, us-west-1]. To allow this add " +
- "<allow until='yyyy-mm-dd'>global-endpoint-change</allow> to validation-overrides.xml, see " +
- "https://docs.vespa.ai/en/reference/validation-overrides.html", e.getMessage());
- }
-
- // Redeploy with override succeeds
- applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "bar")
- .region(west.region().value())
- .region(east.region().value())
- .allow(ValidationId.globalEndpointChange)
- .build();
- context.submit(applicationPackage).deploy();
- assertEquals(ClusterSpec.Id.from("bar"), tester.applications().requireInstance(context.instanceId())
- .rotations().get(0).clusterId());
- }
-
- @Test
- void testZoneEndpointChanges() {
- DeploymentContext app = tester.newDeploymentContext();
- // Set up app with default settings.
- app.submit(ApplicationPackageBuilder.fromDeploymentXml("""
- <deployment>
- <prod>
- <region>us-east-3</region>
- </prod>
- </deployment>"""));
-
- assertEquals("zone-endpoint-change: application 'tenant.application' has a public endpoint for " +
- "cluster 'foo' in 'us-east-3', but the new deployment spec disables this. " +
- "To allow this add <allow until='yyyy-mm-dd'>zone-endpoint-change</allow> to validation-overrides.xml, " +
- "see https://docs.vespa.ai/en/reference/validation-overrides.html",
- assertThrows(IllegalArgumentException.class,
- () -> app.submit(ApplicationPackageBuilder.fromDeploymentXml("""
- <deployment>
- <prod>
- <region>us-east-3</region>
- </prod>
- <endpoints>
- <endpoint type='zone' container-id='foo' enabled='false' />
- </endpoints>
- </deployment>""")))
- .getMessage());
-
- // Disabling endpoints is OK with override.
- app.submit(ApplicationPackageBuilder.fromDeploymentXml("""
- <deployment>
- <prod>
- <region>us-east-3</region>
- </prod>
- <endpoints>
- <endpoint type='zone' container-id='foo' enabled='false' />
- </endpoints>
- </deployment>""",
- ValidationId.zoneEndpointChange));
-
- // Enabling endpoints again is OK, as is adding a private endpoint with some URN.
- app.submit(ApplicationPackageBuilder.fromDeploymentXml("""
- <deployment>
- <prod>
- <region>us-east-3</region>
- </prod>
- <endpoints>
- <endpoint type='private' container-id='foo'>
- <allow with='aws-private-link' arn='yarn' />
- </endpoint>
- </endpoints>
- </deployment>""",
- ValidationId.zoneEndpointChange));
-
- // Changing URNs is guarded.
- assertEquals("zone-endpoint-change: application 'tenant.application' allows access to cluster " +
- "'foo' in 'us-east-3' to ['yarn' through 'aws-private-link'], " +
- "but does not include all these in the new deployment spec. " +
- "Deploying with the new settings will allow access to ['yarn' through 'gcp-service-connect']. " +
- "To allow this add <allow until='yyyy-mm-dd'>zone-endpoint-change</allow> to validation-overrides.xml, " +
- "see https://docs.vespa.ai/en/reference/validation-overrides.html",
- assertThrows(IllegalArgumentException.class,
- () -> app.submit(ApplicationPackageBuilder.fromDeploymentXml("""
- <deployment>
- <prod>
- <region>us-east-3</region>
- </prod>
- <endpoints>
- <endpoint type='private' container-id='foo'>
- <allow with='gcp-service-connect' project='yarn' />
- </endpoint>
- </endpoints>
- </deployment>""")))
- .getMessage());
-
- // Changing cluster, effectively removing old URNs, is also guarded.
- assertEquals("zone-endpoint-change: application 'tenant.application' allows access to cluster 'foo' in 'us-east-3' to " +
- "['yarn' through 'aws-private-link'], but does not include all these in the new deployment spec. " +
- "Deploying with the new settings will allow access to no one",
- assertThrows(IllegalArgumentException.class,
- () -> app.submit(ApplicationPackageBuilder.fromDeploymentXml("""
- <deployment>
- <prod>
- <region>us-east-3</region>
- </prod>
- <endpoints>
- <endpoint type='private' container-id='bar'>
- <allow with='aws-private-link' arn='yarn' />
- </endpoint>
- </endpoints>
- </deployment>""")))
- .getMessage());
- }
-
-
- @Test
- void testReadableApplications() {
- var db = new MockCuratorDb(tester.controller().system());
- var tester = new DeploymentTester(new ControllerTester(db));
-
- // Create and deploy two applications
- var app1 = tester.newDeploymentContext("t1", "a1", "default")
- .submit()
- .deploy();
- var app2 = tester.newDeploymentContext("t2", "a2", "default")
- .submit()
- .deploy();
- assertEquals(2, tester.applications().readable().size());
-
- // Write invalid data to one application
- db.curator().set(Path.fromString("/controller/v1/applications/" + app2.application().id().serialized()),
- new byte[]{(byte) 0xDE, (byte) 0xAD});
-
- // Can read the remaining readable
- assertEquals(1, tester.applications().readable().size());
-
- // Unconditionally reading all applications fails
- try {
- tester.applications().asList();
- fail("Expected exception");
- } catch (Exception ignored) {
- }
-
- // Deployment for readable application still succeeds
- app1.submit().deploy();
- }
-
- @Test
- void testClashingEndpointIdAndInstanceName() {
- String deploymentXml = "<deployment version='1.0' athenz-domain='domain' athenz-service='service'>\n" +
- " <instance id=\"default\">\n" +
- " <prod>\n" +
- " <region active=\"true\">us-west-1</region>\n" +
- " </prod>\n" +
- " <endpoints>\n" +
- " <endpoint id=\"dev\" container-id=\"qrs\"/>\n" +
- " </endpoints>\n" +
- " </instance>\n" +
- " <instance id=\"dev\">\n" +
- " <prod>\n" +
- " <region active=\"true\">us-west-1</region>\n" +
- " </prod>\n" +
- " <endpoints>\n" +
- " <endpoint id=\"default\" container-id=\"qrs\"/>\n" +
- " </endpoints>\n" +
- " </instance>\n" +
- "</deployment>\n";
- ApplicationPackage applicationPackage = ApplicationPackageBuilder.fromDeploymentXml(deploymentXml);
- try {
- tester.newDeploymentContext().submit(applicationPackage);
- fail("Expected exception");
- } catch (IllegalArgumentException e) {
- assertEquals("Endpoint with ID 'default' in instance 'dev' clashes with endpoint 'dev' in instance 'default'",
- e.getMessage());
- }
- }
-
- @Test
- void testTestPackageWarnings() {
- String deploymentXml = "<deployment version='1.0'>\n" +
- " <prod>\n" +
- " <region>us-west-1</region>\n" +
- " </prod>\n" +
- "</deployment>\n";
- ApplicationPackage applicationPackage = ApplicationPackageBuilder.fromDeploymentXml(deploymentXml);
- byte[] testPackage = ApplicationPackage.filesZip(Map.of("tests/staging-test/foo.json", new byte[0]));
- var app = tester.newDeploymentContext();
- tester.jobs().submit(app.application().id(), new Submission(applicationPackage, testPackage, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Instant.EPOCH, 0), 1);
- assertEquals(List.of(new Notification(tester.clock().instant(),
- Type.testPackage,
- Level.warning,
- NotificationSource.from(app.application().id()),
- "There are problems with tests for [application](https://console.tld/tenant/tenant/application/application/prod/instance)",
- List.of("test package has staging tests, so it should also include staging setup",
- "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"))),
- tester.controller().notificationsDb().listNotifications(NotificationSource.from(app.application().id()), true));
- }
-
- @Test
- void testCompileVersion() {
- DeploymentContext context = tester.newDeploymentContext();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder().region("us-west-1").build();
- TenantAndApplicationId application = TenantAndApplicationId.from(context.instanceId());
-
- // No deployments result in system version
- Version version0 = Version.fromString("7.1");
- tester.controllerTester().upgradeSystem(version0);
- tester.upgrader().overrideConfidence(version0, Confidence.normal);
- tester.controllerTester().computeVersionStatus();
- Version version00 = Version.fromString("6.2");
- assertEquals(version00, tester.applications().compileVersion(application, OptionalInt.of(7)));
- assertEquals(version00, tester.applications().compileVersion(application, OptionalInt.empty()));
- tester.clock().advance(Duration.ofSeconds(10801));
- tester.controllerTester().computeVersionStatus();
- assertEquals(version0, tester.applications().compileVersion(application, OptionalInt.of(7)));
- assertEquals(version0, tester.applications().compileVersion(application, OptionalInt.empty()));
- assertEquals("this system has no available versions on specified major: 8",
- assertThrows(IllegalArgumentException.class,
- () -> tester.applications().compileVersion(application, OptionalInt.of(8)))
- .getMessage());
- context.submit(applicationPackage).deploy();
-
- // System is upgraded
- Version version1 = Version.fromString("7.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.clock().advance(Duration.ofSeconds(10801));
- tester.upgrader().overrideConfidence(version1, Confidence.normal);
- tester.controllerTester().computeVersionStatus();
- assertEquals(version0, tester.applications().compileVersion(application, OptionalInt.empty()));
-
- // Application is upgraded and compile version is bumped
- tester.upgrader().maintain();
- context.deployPlatform(version1);
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.empty()));
-
- DeploymentContext legacyApp = tester.newDeploymentContext("avoid", "gc", "default").submit().deploy();
- TenantAndApplicationId newApp = TenantAndApplicationId.from("new", "app");
-
- // A new major is released to the system
- Version version2 = Version.fromString("8.0");
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().overrideConfidence(version2, Confidence.low);
- tester.clock().advance(Duration.ofSeconds(10801));
- tester.controllerTester().computeVersionStatus();
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.empty()));
- assertEquals("this system has no available versions on specified major: 8",
- assertThrows(IllegalArgumentException.class,
- () -> tester.applications().compileVersion(application, OptionalInt.of(8)))
- .getMessage());
-
- tester.upgrader().overrideConfidence(version2, Confidence.normal);
- tester.controllerTester().computeVersionStatus();
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.empty()));
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.of(8)));
- assertEquals(version2, tester.applications().compileVersion(newApp, OptionalInt.empty()));
-
- // The new major is marked as incompatible with older compile versions
- tester.controllerTester().flagSource().withListFlag(PermanentFlags.INCOMPATIBLE_VERSIONS.id(), List.of("8"), String.class);
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.of(7)));
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.empty()));
- assertEquals(version2, tester.applications().compileVersion(application, OptionalInt.of(8)));
- assertEquals(version2, tester.applications().compileVersion(newApp, OptionalInt.empty()));
-
- // The only version on major 8 has low confidence.
- tester.upgrader().overrideConfidence(version2, Confidence.low);
- tester.controllerTester().computeVersionStatus();
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.of(7)));
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.empty()));
- assertEquals("this system has no available versions on specified major: 8",
- assertThrows(IllegalArgumentException.class,
- () -> tester.applications().compileVersion(application, OptionalInt.of(8)))
- .getMessage());
- assertEquals(version1, tester.applications().compileVersion(newApp, OptionalInt.empty()));
- assertEquals(version1, tester.applications().compileVersion(newApp, OptionalInt.empty()));
-
- // Version on major 8 has normal confidence again
- tester.upgrader().overrideConfidence(version2, Confidence.normal);
- tester.controllerTester().computeVersionStatus();
-
- // Application upgrades to major 8; major version from deployment spec should cause a downgrade.
- context.submit(new ApplicationPackageBuilder().region("us-west-1").compileVersion(version2).build()).deploy();
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.of(7)));
- assertEquals(version2, tester.applications().compileVersion(application, OptionalInt.empty()));
-
- // Reduced confidence should not cause a downgrade.
- tester.upgrader().overrideConfidence(version2, Confidence.low);
- tester.controllerTester().computeVersionStatus();
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.of(7)));
- assertEquals(version2, tester.applications().compileVersion(application, OptionalInt.empty()));
- assertEquals(version2, tester.applications().compileVersion(application, OptionalInt.of(8)));
-
- // All versions on new major having broken confidence makes it all fail for upgraded apps, but this shouldn't happen in practice.
- tester.upgrader().overrideConfidence(version2, Confidence.broken);
- tester.controllerTester().computeVersionStatus();
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.of(7)));
- assertEquals("no suitable, released compile version exists",
- assertThrows(IllegalArgumentException.class,
- () -> tester.applications().compileVersion(application, OptionalInt.empty()))
- .getMessage());
- assertEquals("no suitable, released compile version exists for specified major: 8",
- assertThrows(IllegalArgumentException.class,
- () -> tester.applications().compileVersion(application, OptionalInt.of(8)))
- .getMessage());
-
- // Major versions are not incompatible anymore, so the old compile version should work again.
- tester.controllerTester().flagSource().withListFlag(PermanentFlags.INCOMPATIBLE_VERSIONS.id(), List.of(), String.class);
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.of(7)));
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.empty()));
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.of(8)));
-
- // Simply reduced confidence shouldn't cause any changes.
- tester.upgrader().overrideConfidence(version2, Confidence.low);
- tester.controllerTester().computeVersionStatus();
- assertEquals(version1, tester.applications().compileVersion(application, OptionalInt.of(7)));
- assertEquals(version2, tester.applications().compileVersion(application, OptionalInt.empty()));
- assertEquals(version2, tester.applications().compileVersion(application, OptionalInt.of(8)));
- }
-
- @Test
- void testCloudAccount() {
- DeploymentContext context = tester.newDeploymentContext();
- ZoneId devZone = devAwsUsEast2a.zone();
- ZoneId prodZone = productionAwsUsEast1a.zone();
- String cloudAccount = "aws:012345678912";
- var applicationPackage = new ApplicationPackageBuilder()
- .cloudAccount(cloudAccount)
- .region(prodZone.region())
- .build();
-
- // Submission fails because cloud account is not declared for this tenant
- assertEquals("cloud accounts [aws:012345678912] are not valid for tenant tenant",
- assertThrows(IllegalArgumentException.class,
- () -> context.submit(applicationPackage))
- .getMessage());
- assertEquals("cloud accounts [aws:012345678912] are not valid for tenant tenant",
- assertThrows(IllegalArgumentException.class,
- () -> context.runJob(devZone, applicationPackage))
- .getMessage());
-
- // Deployment fails because zone is not configured in requested cloud account
- tester.controllerTester().flagSource().withListFlag(PermanentFlags.CLOUD_ACCOUNTS.id(), List.of(cloudAccount), String.class);
- assertEquals("Zone prod.aws-us-east-1a is not configured in requested cloud account 'aws:012345678912'",
- assertThrows(IllegalArgumentException.class,
- () -> context.submit(applicationPackage))
- .getMessage());
-
- context.runJob(devUsEast1, applicationPackage); // OK, because no special account is used.
- assertEquals("Zone dev.aws-us-east-2a is not configured in requested cloud account 'aws:012345678912'",
- assertThrows(IllegalArgumentException.class,
- () -> context.runJob(devZone, applicationPackage))
- .getMessage());
-
- // Deployment to prod succeeds once all zones are configured in requested account
- tester.controllerTester().zoneRegistry().configureCloudAccount(CloudAccount.from(cloudAccount), prodZone);
- context.submit(applicationPackage).deploy();
-
- // Dev zone is added as a configured zone and deployment succeeds
- tester.controllerTester().zoneRegistry().configureCloudAccount(CloudAccount.from(cloudAccount), devZone);
- context.runJob(devZone, applicationPackage);
-
- // All deployments use the custom account
- for (var zoneId : List.of(devZone, prodZone)) {
- assertEquals(cloudAccount, tester.controllerTester().configServer()
- .cloudAccount(context.deploymentIdIn(zoneId))
- .get().value());
- }
- // Tests are run in the default cloud, however, where the default cloud account is used
- for (var zoneId : List.of(systemTest.zone(), stagingTest.zone())) {
- assertEquals(Optional.empty(), tester.controllerTester().configServer()
- .cloudAccount(context.deploymentIdIn(zoneId)));
- }
- }
-
- @Test
- void testCloudAccountWithDefaultOverride() {
- var context = tester.newDeploymentContext();
- var prodZone1 = productionAwsUsEast1a.zone();
- var prodZone2 = productionUsWest1.zone();
- var cloudAccount = "aws:012345678912";
- var application = new ApplicationPackageBuilder()
- .cloudAccount(cloudAccount)
- .region(prodZone1.region())
- .region(prodZone2.region(), "default")
- .build();
-
- // Allow use of custom account (test, staging and zone 1)
- tester.controllerTester().flagSource().withListFlag(PermanentFlags.CLOUD_ACCOUNTS.id(), List.of(cloudAccount), String.class);
-
- // Deployment to prod succeeds once all zones are configured in requested account
- tester.controllerTester().zoneRegistry().configureCloudAccount(CloudAccount.from(cloudAccount),
- systemTest.zone(),
- stagingTest.zone(),
- prodZone1);
-
- context.submit(application).deploy();
-
- assertEquals(cloudAccount, tester.controllerTester().configServer().cloudAccount(context.deploymentIdIn(prodZone1)).get().value());
- assertEquals(Optional.empty(), tester.controllerTester().configServer().cloudAccount(context.deploymentIdIn(prodZone2)));
- }
-
- @Test
- void testDeactivateDeploymentUnknownByController() {
- DeploymentContext context = tester.newDeploymentContext();
- DeploymentId deployment = context.deploymentIdIn(ZoneId.from("prod", "us-west-1"));
- DeploymentData deploymentData = new DeploymentData(deployment.applicationId(), deployment.zoneId(), InputStream::nullInputStream, Version.fromString("6.1"),
- () -> DeploymentEndpoints.none, Optional.empty(), Optional.empty(),
- Quota::unlimited, List.of(), List.of(), Optional::empty, () -> List.of(), false);
- tester.configServer().deploy(deploymentData);
- assertTrue(tester.configServer().application(deployment.applicationId(), deployment.zoneId()).isPresent());
- tester.controller().applications().deactivate(deployment.applicationId(), deployment.zoneId());
- assertFalse(tester.configServer().application(deployment.applicationId(), deployment.zoneId()).isPresent());
- }
-
- @Test
- void testVerifyPlan() {
- DeploymentId deployment = tester.newDeploymentContext().deploymentIdIn(ZoneId.from("prod", "us-west-1"));
- TenantName tenant = deployment.applicationId().tenant();
-
- tester.controller().serviceRegistry().billingController().setPlan(tenant, PlanRegistryMock.nonePlan.id(), false, false);
- try {
- tester.controller().applications().verifyPlan(tenant);
- fail("should have thrown an exception");
- } catch (IllegalArgumentException e) {
- assertEquals("Tenant 'tenant' has a plan 'None Plan - for testing purposes' with zero quota, not allowed to deploy. " +
- "See https://cloud.vespa.ai/support", e.getMessage());
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
deleted file mode 100644
index 7bdecca11c0..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTester.java
+++ /dev/null
@@ -1,403 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.test.ManualClock;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMavenRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockUserManagement;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade;
-import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
-import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock;
-import com.yahoo.vespa.hosted.controller.integration.MetricsMock;
-import com.yahoo.vespa.hosted.controller.integration.SecretStoreMock;
-import com.yahoo.vespa.hosted.controller.integration.ServiceRegistryMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.security.AthenzCredentials;
-import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec;
-import com.yahoo.vespa.hosted.controller.security.Auth0Credentials;
-import com.yahoo.vespa.hosted.controller.security.CloudAccessControl;
-import com.yahoo.vespa.hosted.controller.security.CloudTenantSpec;
-import com.yahoo.vespa.hosted.controller.security.Credentials;
-import com.yahoo.vespa.hosted.controller.security.TenantSpec;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-import com.yahoo.yolean.concurrent.Sleeper;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.ZoneOffset;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import java.util.OptionalLong;
-import java.util.Random;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Consumer;
-import java.util.logging.Handler;
-import java.util.logging.Logger;
-
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * Convenience methods for controller tests.
- *
- * @author bratseth
- * @author mpolden
- */
-public final class ControllerTester {
-
- public static final int availableRotations = 10;
-
- private final boolean inContainer;
- private final AthenzDbMock athenzDb;
- private final ManualClock clock;
- private final ServiceRegistryMock serviceRegistry;
- private final CuratorDb curator;
- private final RotationsConfig rotationsConfig;
- private final InMemoryFlagSource flagSource;
- private final AtomicLong nextPropertyId = new AtomicLong(1000);
- private final AtomicInteger nextProjectId = new AtomicInteger(1000);
- private final AtomicInteger nextDomainId = new AtomicInteger(1000);
- private final AtomicInteger nextMinorVersion = new AtomicInteger(ControllerVersion.CURRENT.version().getMinor() + 1);
-
- private Controller controller;
-
- public ControllerTester(RotationsConfig rotationsConfig, MockCuratorDb curatorDb) {
- this(new AthenzDbMock(),
- curatorDb,
- rotationsConfig,
- new ServiceRegistryMock());
- }
-
- public ControllerTester(ServiceRegistryMock serviceRegistryMock) {
- this(new AthenzDbMock(), new MockCuratorDb(serviceRegistryMock.zoneRegistry().system()), defaultRotationsConfig(), serviceRegistryMock);
- }
-
- public ControllerTester(RotationsConfig rotationsConfig, SystemName system) {
- this(new AthenzDbMock(), new MockCuratorDb(system), rotationsConfig, new ServiceRegistryMock(system));
- }
-
- public ControllerTester(MockCuratorDb curatorDb) {
- this(defaultRotationsConfig(), curatorDb);
- }
-
- public ControllerTester() {
- this(defaultRotationsConfig(), new MockCuratorDb(new ServiceRegistryMock().zoneRegistry().system()));
- }
-
- public ControllerTester(SystemName system) {
- this(new AthenzDbMock(), new MockCuratorDb(system), defaultRotationsConfig(), new ServiceRegistryMock(system));
- }
-
- private ControllerTester(AthenzDbMock athenzDb, boolean inContainer, CuratorDb curator,
- RotationsConfig rotationsConfig, ServiceRegistryMock serviceRegistry,
- InMemoryFlagSource flagSource, Controller controller) {
- this.athenzDb = athenzDb;
- this.inContainer = inContainer;
- this.clock = serviceRegistry.clock();
- this.serviceRegistry = serviceRegistry;
- this.curator = curator;
- this.rotationsConfig = rotationsConfig;
- this.flagSource = flagSource.withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true)
- .withListFlag(PermanentFlags.INCOMPATIBLE_VERSIONS.id(), List.of(), String.class);
- this.controller = controller;
-
- // Make root logger use time from manual clock
- configureDefaultLogHandler(handler -> handler.setFilter(
- record -> {
- record.setInstant(clock.instant());
- return true;
- }));
- }
-
- private ControllerTester(AthenzDbMock athenzDb,
- CuratorDb curator, RotationsConfig rotationsConfig,
- ServiceRegistryMock serviceRegistry) {
- this(athenzDb, curator, rotationsConfig, serviceRegistry, new InMemoryFlagSource());
- }
-
- private ControllerTester(AthenzDbMock athenzDb,
- CuratorDb curator, RotationsConfig rotationsConfig,
- ServiceRegistryMock serviceRegistry, InMemoryFlagSource flagSource) {
- this(athenzDb, false, curator, rotationsConfig, serviceRegistry, flagSource,
- createController(curator, rotationsConfig, athenzDb, serviceRegistry, flagSource));
- }
-
- /** Creates a ControllerTester built on the ContainerTester's controller. This controller can not be recreated. */
- public ControllerTester(ContainerTester tester) {
- this(tester.athenzClientFactory().getSetup(),
- true,
- tester.controller().curator(),
- null,
- tester.serviceRegistry(),
- tester.flagSource(),
- tester.controller());
- }
-
-
- public void configureDefaultLogHandler(Consumer<Handler> configureFunc) {
- Arrays.stream(Logger.getLogger("").getHandlers())
- // Do not mess with log configuration if a custom one has been set
- .filter(ignored -> System.getProperty("java.util.logging.config.file") == null)
- .forEach(configureFunc);
- }
-
- public Controller controller() { return controller; }
-
- public CuratorDb curator() { return curator; }
-
- public ManualClock clock() { return clock; }
-
- public AthenzDbMock athenzDb() { return athenzDb; }
-
- public InMemoryFlagSource flagSource() { return flagSource; }
-
- public MemoryNameService nameService() {
- return serviceRegistry.nameService();
- }
-
- public ZoneRegistryMock zoneRegistry() { return serviceRegistry.zoneRegistry(); }
-
- public ConfigServerMock configServer() { return serviceRegistry.configServerMock(); }
-
- public ServiceRegistryMock serviceRegistry() { return serviceRegistry; }
-
- public Optional<Record> findCname(String name) {
- return serviceRegistry.nameService().findRecords(Record.Type.CNAME, RecordName.from(name)).stream().findFirst();
- }
-
- /**
- * Returns a version suitable as the next system version, i.e. a version that is always higher than the compiled-in
- * controller version.
- */
- public Version nextVersion() {
- var current = ControllerVersion.CURRENT.version();
- return new Version(current.getMajor(), nextMinorVersion.getAndIncrement(), current.getMicro());
- }
-
- /** Set the zones and system for this and bootstrap infrastructure nodes */
- public ControllerTester setZones(List<ZoneId> zones) {
- ZoneApiMock.Builder builder = ZoneApiMock.newBuilder().withSystem(zoneRegistry().system());
- zoneRegistry().setZones(zones.stream().map(zone -> builder.with(zone).build()).toList());
- configServer().bootstrap(zones, SystemApplication.notController());
- return this;
- }
-
- /** Set the routing method for given zones */
- public ControllerTester setRoutingMethod(List<ZoneId> zones, RoutingMethod routingMethod) {
- zoneRegistry().setRoutingMethod(zones.stream().map(ZoneApiMock::from).toList(),
- routingMethod);
- return this;
- }
-
- /** Create a new controller instance. Useful to verify that controller state is rebuilt from persistence */
- public void createNewController() {
- if (inContainer)
- throw new UnsupportedOperationException("Cannot recreate this controller");
- controller = createController(curator, rotationsConfig, athenzDb, serviceRegistry, flagSource);
- }
-
- /** Upgrade controller to given version */
- public void upgradeController(Version version, String commitSha, Instant commitDate) {
- for (var hostname : controller().curator().cluster()) {
- upgradeController(HostName.of(hostname), version, commitSha, commitDate);
- }
- }
-
- /** Upgrade controller to given version */
- public void upgradeController(HostName hostname, Version version, String commitSha, Instant commitDate) {
- controller().curator().writeControllerVersion(hostname, new ControllerVersion(version, commitSha, commitDate));
- computeVersionStatus();
- }
-
- public void upgradeController(Version version) {
- upgradeController(version, "badc0ffee", Instant.EPOCH);
- }
-
- /** Upgrade system applications in all zones to given version */
- public void upgradeSystemApplications(Version version) {
- upgradeSystemApplications(version, SystemApplication.notController());
- }
-
- /** Upgrade given system applications in all zones to version */
- public void upgradeSystemApplications(Version version, List<SystemApplication> systemApplications) {
- for (ZoneApi zone : zoneRegistry().zones().all().zones()) {
- for (SystemApplication application : systemApplications) {
- if (!application.hasApplicationPackage()) {
- configServer().nodeRepository().upgrade(zone.getId(), application.nodeType(), version, false);
- }
- configServer().setVersion(version, application.id(), zone.getId());
- configServer().convergeServices(application.id(), zone.getId());
- }
- }
- computeVersionStatus();
- }
-
- /** Upgrade entire system to given version */
- public void upgradeSystem(Version version) {
- ((MockMavenRepository) controller.mavenRepository()).addVersion(version);
- upgradeController(version);
- upgradeSystemApplications(version);
- }
-
- /** Re-compute and write version status */
- public void computeVersionStatus() {
- controller().updateVersionStatus(VersionStatus.compute(controller()));
- }
-
- public int hourOfDayAfter(Duration duration) {
- clock().advance(duration);
- return controller().clock().instant().atOffset(ZoneOffset.UTC).getHour();
- }
-
- public AthenzDomain createDomainWithAdmin(String domainName, AthenzUser user) {
- AthenzDomain domain = new AthenzDomain(domainName);
- athenzDb.getOrCreateDomain(domain).admin(user);
- return domain;
- }
-
- public TenantName createTenant(String tenantName) {
- return createTenant(tenantName, zoneRegistry().system().isPublic() ? Tenant.Type.cloud : Tenant.Type.athenz);
- }
-
- public TenantName createTenant(String tenantName, Tenant.Type type) {
- return switch (type) {
- case athenz -> createTenant(tenantName, "domain" + nextDomainId.getAndIncrement(), nextPropertyId.getAndIncrement());
- case cloud -> createCloudTenant(tenantName);
- default -> throw new UnsupportedOperationException();
- };
- }
-
- public TenantName createTenant(String tenantName, String domainName, Long propertyId) {
- return createAthenzTenant(tenantName, domainName, propertyId, Optional.empty());
- }
-
- private TenantName createAthenzTenant(String tenantName, String domainName, Long propertyId, Optional<Contact> contact) {
- TenantName name = TenantName.from(tenantName);
- Optional<Tenant> existing = controller().tenants().get(name);
- if (existing.isPresent()) return name;
- AthenzUser user = new AthenzUser("user");
- AthenzDomain domain = createDomainWithAdmin(domainName, user);
- AthenzTenantSpec tenantSpec = new AthenzTenantSpec(name,
- domain,
- new Property("Property" + propertyId),
- Optional.ofNullable(propertyId).map(Object::toString).map(PropertyId::new));
- AthenzCredentials credentials = new AthenzCredentials(
- new AthenzPrincipal(user), domain, OAuthCredentials.createForTesting("okta-access-token", "okta-identity-token"));
- controller().tenants().create(tenantSpec, credentials);
- contact.ifPresent(value -> controller().tenants().lockOrThrow(name, LockedTenant.Athenz.class, tenant ->
- controller().tenants().store(tenant.with(value))));
- assertNotNull(controller().tenants().get(name));
- return name;
- }
-
- private TenantName createCloudTenant(String tenantName) {
- TenantName tenant = TenantName.from(tenantName);
- TenantSpec spec = new CloudTenantSpec(tenant, "token");
- controller().tenants().create(spec, new Auth0Credentials(new SimplePrincipal("dev-" + tenantName), Set.of(Role.administrator(tenant))));
- return tenant;
- }
-
- public Credentials credentialsFor(TenantName tenantName) {
- Tenant tenant = controller().tenants().require(tenantName);
-
- return switch (tenant.type()) {
- case athenz -> new AthenzCredentials(new AthenzPrincipal(new AthenzUser("user")),
- ((AthenzTenant) tenant).domain(),
- OAuthCredentials.createForTesting("okta-access-token", "okta-identity-token"));
- case cloud -> new Credentials(new SimplePrincipal("dev"));
- default -> throw new IllegalArgumentException("Unexpected tenant type '" + tenant.type() + "'");
- };
- }
-
- public Application createApplication(ApplicationId id) {
- return createApplication(id.tenant().value(), id.application().value(), id.instance().value());
- }
-
- public Application createApplication(String tenant, String applicationName, String instanceName) {
- Application application = createApplication(tenant, applicationName);
- controller().applications().createInstance(application.id().instance(instanceName));
- return application;
- }
-
- public Application createApplication(String tenant, String applicationName) {
- TenantAndApplicationId applicationId = TenantAndApplicationId.from(tenant, applicationName);
- controller().applications().getApplication(applicationId)
- .orElseGet(() -> controller().applications().createApplication(applicationId, credentialsFor(applicationId.tenant())));
- controller().applications().lockApplicationOrThrow(applicationId, app ->
- controller().applications().store(app.withProjectId(OptionalLong.of(nextProjectId.getAndIncrement()))));
- Application application = controller().applications().requireApplication(applicationId);
- assertTrue(application.projectId().isPresent());
- return application;
- }
-
- private static Controller createController(CuratorDb curator, RotationsConfig rotationsConfig,
- AthenzDbMock athensDb,
- ServiceRegistryMock serviceRegistry,
- FlagSource flagSource) {
- Random random = new Random(serviceRegistry.clock().instant().toEpochMilli()); // Seed with clock for test determinism
- Controller controller = new Controller(curator,
- rotationsConfig,
- serviceRegistry.zoneRegistry().system().isPublic() ?
- new CloudAccessControl(new MockUserManagement(), flagSource, serviceRegistry) :
- new AthenzFacade(new AthenzClientFactoryMock(athensDb)),
- flagSource,
- new MockMavenRepository(serviceRegistry.clock()),
- serviceRegistry,
- new MetricsMock(), new SecretStoreMock(),
- new ControllerConfig.Builder().build(),
- Sleeper.NOOP,
- random,
- random);
- // Calculate initial versions
- controller.updateVersionStatus(VersionStatus.compute(controller));
- return controller;
- }
-
- private static RotationsConfig defaultRotationsConfig() {
- RotationsConfig.Builder builder = new RotationsConfig.Builder();
- for (int i = 1; i <= availableRotations; i++) {
- String id = Text.format("%02d", i);
- builder.rotations("rotation-id-" + id, "rotation-fqdn-" + id);
- }
- return new RotationsConfig(builder);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/MailVerifierTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/MailVerifierTest.java
deleted file mode 100644
index 4fbf39f8d8b..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/MailVerifierTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
-import com.yahoo.vespa.hosted.controller.application.MailVerifier;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Email;
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.tenant.TenantBilling;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContact;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-
-/**
- * @author olaa
- */
-class MailVerifierTest {
-
- private final ControllerTester tester = new ControllerTester(SystemName.Public);
- private final MockMailer mailer = tester.serviceRegistry().mailer();
- private final MailVerifier mailVerifier = new MailVerifier(tester.serviceRegistry().consoleUrls(), tester.controller().tenants(), mailer, tester.curator(), tester.clock());
-
- private static final TenantName tenantName = TenantName.from("scoober");
- private static final String mail = "unverified@bar.com";
- private static final List<TenantContacts.Audience> audiences = List.of(TenantContacts.Audience.NOTIFICATIONS, TenantContacts.Audience.TENANT);
-
- @BeforeEach
- public void setup() {
- tester.createTenant(tenantName.value(), Tenant.Type.cloud);
-
- tester.controller().tenants().lockOrThrow(tenantName, LockedTenant.Cloud.class, lockedTenant -> {
- var contacts = List.of(
- new TenantContacts.EmailContact(audiences, new Email("verified@bar.com", true)),
- new TenantContacts.EmailContact(audiences, new Email(mail, false)),
- new TenantContacts.EmailContact(audiences, new Email("another-unverified@bar.com", false))
- );
- lockedTenant = lockedTenant.withInfo(lockedTenant.get().info().withContacts(new TenantContacts(contacts)));
- tester.controller().tenants().store(lockedTenant);
- });
- }
-
- @Test
- public void test_new_mail_verification() {
- mailVerifier.sendMailVerification(tenantName, mail, PendingMailVerification.MailType.NOTIFICATIONS);
-
- // Verify mail is sent
- assertEquals(1, mailer.inbox(mail).size());
-
- // Verify ZK data is updated
- var writtenMailVerification = tester.curator().listPendingMailVerifications().get(0);
- assertEquals(PendingMailVerification.MailType.NOTIFICATIONS, writtenMailVerification.getMailType());
- assertEquals(tenantName, writtenMailVerification.getTenantName());
- assertEquals(tester.clock().instant().plus(Duration.ofDays(7)), writtenMailVerification.getVerificationDeadline());
- assertEquals(mail, writtenMailVerification.getMailAddress());
-
- // Mail verification is no-op if deadline has passed
- tester.clock().advance(Duration.ofDays(14));
- assertFalse(mailVerifier.verifyMail(writtenMailVerification.getVerificationCode()));
- assertFalse(tester.curator().listPendingMailVerifications().isEmpty());
-
- // Mail is verified
- tester.clock().retreat(Duration.ofDays(14));
- mailVerifier.verifyMail(writtenMailVerification.getVerificationCode());
- assertTrue(tester.curator().listPendingMailVerifications().isEmpty());
- var tenant = tester.controller().tenants().require(tenantName, CloudTenant.class);
- var expectedContacts = List.of(
- new TenantContacts.EmailContact(audiences, new Email("verified@bar.com", true)),
- new TenantContacts.EmailContact(audiences, new Email(mail, true)),
- new TenantContacts.EmailContact(audiences, new Email("another-unverified@bar.com", false))
- );
- assertEquals(expectedContacts, tenant.info().contacts().all());
- }
-
- @Test
- public void resending_verification_deletes_old_one() {
- var pendingMailVerification = mailVerifier.sendMailVerification(tenantName, mail, PendingMailVerification.MailType.NOTIFICATIONS);
- var tenant = tester.controller().tenants().require(tenantName, CloudTenant.class);
-
- // Unknown mail is no-op
- var resentVerification = mailVerifier.resendMailVerification(tenantName, "unknown-mail", PendingMailVerification.MailType.NOTIFICATIONS);
- assertTrue(resentVerification.isEmpty());
- assertTrue(tester.curator().getPendingMailVerification(pendingMailVerification.getVerificationCode()).isPresent());
-
- // Verification mail is re-sent, old data is replaced
- resentVerification = mailVerifier.resendMailVerification(tenantName, mail, PendingMailVerification.MailType.NOTIFICATIONS);
- assertTrue(resentVerification.isPresent());
- assertTrue(tester.curator().getPendingMailVerification(pendingMailVerification.getVerificationCode()).isEmpty());
- assertTrue(tester.curator().getPendingMailVerification(resentVerification.get().getVerificationCode()).isPresent());
- }
-
- @Test
- public void test_billing_mail_verification() {
- var billingMail = "billing@foo.bar";
- tester.controller().tenants().lockOrThrow(tenantName, LockedTenant.Cloud.class, lockedTenant -> {
- var tenantBilling = TenantBilling.empty().withContact(TenantContact.empty().withEmail(new Email(billingMail, false)));
- lockedTenant = lockedTenant.withInfo(lockedTenant.get().info().withBilling(tenantBilling));
- tester.controller().tenants().store(lockedTenant);
- });
- mailVerifier.sendMailVerification(tenantName, billingMail, PendingMailVerification.MailType.BILLING);
-
- // Assert written verification data
- var writtenMailVerification = tester.curator().listPendingMailVerifications().get(0);
- assertEquals(PendingMailVerification.MailType.BILLING, writtenMailVerification.getMailType());
- assertEquals(tenantName, writtenMailVerification.getTenantName());
- assertEquals(tester.clock().instant().plus(Duration.ofDays(7)), writtenMailVerification.getVerificationDeadline());
- assertEquals(billingMail, writtenMailVerification.getMailAddress());
-
- // Assert mail is verified
- mailVerifier.verifyMail(writtenMailVerification.getVerificationCode());
- assertTrue(tester.curator().listPendingMailVerifications().isEmpty());
- var tenant = tester.controller().tenants().require(tenantName, CloudTenant.class);
- var expectedBillingContact = TenantContact.empty().withEmail(new Email(billingMail, true));
- assertEquals(expectedBillingContact, tenant.info().billingContact().contact());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java
deleted file mode 100644
index b20da8ae4d9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/DeploymentQuotaCalculatorTest.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Quota;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.noderepository.ApplicationData;
-import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
-import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
-import org.junit.jupiter.api.Test;
-
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.time.Instant;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalInt;
-import java.util.OptionalLong;
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class DeploymentQuotaCalculatorTest {
-
- @Test
- void quota_is_divided_among_prod_instances() {
- Quota calculated = DeploymentQuotaCalculator.calculate(Quota.unlimited().withBudget(10), List.of(), ApplicationId.defaultId(), ZoneId.defaultId(),
- DeploymentSpec.fromXml(
- """
- <deployment version='1.0'>
- <instance id='instance1'>\s
- <test />
- <staging />
- <prod>
- <region active="true">us-east-1</region>
- <region active="false">us-west-1</region>
- </prod>
- </instance>
- <instance id='instance2'>
- <perf/>
- <dev/>
- <prod>
- <region active="true">us-north-1</region>
- </prod>
- </instance>
- </deployment>"""));
- assertEquals(10d / 3, calculated.budget().orElseThrow().doubleValue(), 1e-5);
- }
-
- @Test
- void quota_is_divided_among_prod_and_manual_instances() {
-
- var existing_dev_deployment = new Application(TenantAndApplicationId.from(ApplicationId.defaultId()), Instant.EPOCH, DeploymentSpec.empty, ValidationOverrides.empty, Optional.empty(),
- Optional.empty(), Optional.empty(), Optional.empty(), OptionalInt.empty(), new ApplicationMetrics(1, 1), Set.of(), OptionalLong.empty(), RevisionHistory.empty(),
- List.of(new Instance(ApplicationId.defaultId()).withNewDeployment(ZoneId.from(Environment.dev, RegionName.defaultName()),
- RevisionId.forProduction(1), Version.emptyVersion, Instant.EPOCH, Map.of(), QuotaUsage.create(0.53d), CloudAccount.empty, List.of())));
-
- Quota calculated = DeploymentQuotaCalculator.calculate(Quota.unlimited().withBudget(2), List.of(existing_dev_deployment), ApplicationId.defaultId(), ZoneId.defaultId(),
- DeploymentSpec.fromXml(
- """
- <deployment version='1.0'>
- <instance id='default'>\s
- <test />
- <staging />
- <prod>
- <region active="true">us-east-1</region>
- <region active="false">us-west-1</region>
- <region active="true">us-north-1</region>
- <region active="true">us-south-1</region>
- </prod>
- </instance>
- </deployment>"""));
- assertEquals((2d - 0.53d) / 4d, calculated.budget().orElseThrow().doubleValue(), 1e-5);
- }
-
- @Test
- void unlimited_quota_remains_unlimited() {
- Quota calculated = DeploymentQuotaCalculator.calculate(Quota.unlimited(), List.of(), ApplicationId.defaultId(), ZoneId.defaultId(), DeploymentSpec.empty);
- assertTrue(calculated.isUnlimited());
- }
-
- @Test
- void zero_quota_remains_zero() {
- Quota calculated = DeploymentQuotaCalculator.calculate(Quota.zero(), List.of(), ApplicationId.defaultId(), ZoneId.defaultId(), DeploymentSpec.empty);
- assertEquals(calculated.budget().orElseThrow().doubleValue(), 0, 1e-5);
- }
-
- @Test
- void using_highest_resource_use() throws Exception {
- var content = new String(Files.readAllBytes(Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/application/response/application.json")));
- var mapper = new ObjectMapper();
- var application = mapper.readValue(content, ApplicationData.class).toApplication();
- var usage = DeploymentQuotaCalculator.calculateQuotaUsage(application);
- assertEquals(1.312, usage.rate(), 0.001);
- }
-
- @Test
- void tenant_quota_in_pipeline() {
- var tenantQuota = Quota.unlimited().withBudget(42);
- var calculated = DeploymentQuotaCalculator.calculate(tenantQuota, List.of(), ApplicationId.defaultId(), ZoneId.from("test", "apac1"), DeploymentSpec.empty);
- assertEquals(tenantQuota, calculated);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java
deleted file mode 100644
index cec48dd1598..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java
+++ /dev/null
@@ -1,396 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.application.Endpoint.Port;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class EndpointTest {
-
- private static final ApplicationId instance1 = ApplicationId.from("t1", "a1", "default");
- private static final ApplicationId instance2 = ApplicationId.from("t2", "a2", "i2");
- private static final TenantAndApplicationId app1 = TenantAndApplicationId.from(instance1);
- private static final TenantAndApplicationId app2 = TenantAndApplicationId.from(instance2);
-
- @Test
- void global_endpoints() {
- DeploymentId deployment1 = new DeploymentId(instance1, ZoneId.from("prod", "us-north-1"));
- DeploymentId deployment2 = new DeploymentId(instance2, ZoneId.from("prod", "us-north-1"));
- ClusterSpec.Id cluster = ClusterSpec.Id.from("default");
- EndpointId endpointId = EndpointId.defaultId();
-
- Map<String, Endpoint> tests = Map.of(
- // Main endpoint with direct routing and default TLS port
- "https://a1.t1.global.vespa.oath.cloud/",
- Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main),
-
- // Main endpoint with custom rotation name
- "https://r1.a1.t1.global.vespa.oath.cloud/",
- Endpoint.of(instance1).target(EndpointId.of("r1"), cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main),
-
- // Main endpoint for custom instance in default rotation
- "https://i2.a2.t2.global.vespa.oath.cloud/",
- Endpoint.of(instance2).target(endpointId, cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main),
-
- // Main endpoint for custom instance with custom rotation name
- "https://r2.i2.a2.t2.global.vespa.oath.cloud/",
- Endpoint.of(instance2).target(EndpointId.of("r2"), cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main),
-
- // Main endpoint in public system
- "https://a1.t1.g.vespa-app.cloud/",
- Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public)
- );
- tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString()));
-
- Map<String, Endpoint> tests2 = Map.of(
- // Default endpoint in public system
- "https://a1.t1.g.vespa-app.cloud/",
- Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public),
-
- // Default endpoint in public CD system
- "https://a1.t1.g.cd.vespa-app.cloud/",
- Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.PublicCd),
-
- // Custom instance in public system
- "https://i2.a2.t2.g.vespa-app.cloud/",
- Endpoint.of(instance2).target(endpointId, cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public)
- );
- tests2.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString()));
- }
-
- @Test
- void global_endpoints_with_endpoint_id() {
- DeploymentId deployment1 = new DeploymentId(instance1, ZoneId.from("prod", "us-north-1"));
- DeploymentId deployment2 = new DeploymentId(instance2, ZoneId.from("prod", "us-north-1"));
- ClusterSpec.Id cluster = ClusterSpec.Id.from("default");
- EndpointId endpointId = EndpointId.defaultId();
-
- Map<String, Endpoint> tests = Map.of(
- // Main endpoint with direct routing and default TLS port
- "https://a1.t1.global.vespa.oath.cloud/",
- Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main),
-
- // Main endpoint with custom rotation name
- "https://r1.a1.t1.global.vespa.oath.cloud/",
- Endpoint.of(instance1).target(EndpointId.of("r1"), cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main),
-
- // Main endpoint for custom instance in default rotation
- "https://i2.a2.t2.global.vespa.oath.cloud/",
- Endpoint.of(instance2).target(endpointId, cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main),
-
- // Main endpoint for custom instance with custom rotation name
- "https://r2.i2.a2.t2.global.vespa.oath.cloud/",
- Endpoint.of(instance2).target(EndpointId.of("r2"), cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.main),
-
- // Main endpoint in public system
- "https://a1.t1.g.vespa-app.cloud/",
- Endpoint.of(instance1).target(endpointId, cluster, List.of(deployment1)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public)
- );
- tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString()));
-
- Map<String, Endpoint> tests2 = Map.of(
- // Custom endpoint and instance in public CD system)
- "https://foo.i2.a2.t2.g.cd.vespa-app.cloud/",
- Endpoint.of(instance2).target(EndpointId.of("foo"), cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.PublicCd),
-
- // Custom endpoint and instance in public system
- "https://foo.i2.a2.t2.g.vespa-app.cloud/",
- Endpoint.of(instance2).target(EndpointId.of("foo"), cluster, List.of(deployment2)).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public)
- );
- tests2.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString()));
- }
-
- @Test
- void zone_endpoints() {
- var cluster = ClusterSpec.Id.from("default"); // Always default for non-direct routing
- var prodZone = new DeploymentId(instance1, ZoneId.from("prod", "us-north-1"));
- var prodZone2 = new DeploymentId(instance2, ZoneId.from("prod", "us-north-1"));
- var testZone = new DeploymentId(instance1, ZoneId.from("test", "us-north-2"));
-
- Map<String, Endpoint> tests = Map.of(
- // Prod endpoint in main
- "https://a1.t1.us-north-1.vespa.oath.cloud/",
- Endpoint.of(instance1).target(cluster, prodZone).on(Port.tls()).in(SystemName.main),
-
- // Prod endpoint in CD
- "https://cd.a1.t1.us-north-1.cd.vespa.oath.cloud/",
- Endpoint.of(instance1).target(cluster, prodZone).on(Port.tls()).in(SystemName.cd),
-
- // Test endpoint in main
- "https://a1.t1.us-north-2.test.vespa.oath.cloud/",
- Endpoint.of(instance1).target(cluster, testZone).on(Port.tls()).in(SystemName.main),
-
- // Non-default cluster in main
- "https://c1.a1.t1.us-north-1.vespa.oath.cloud/",
- Endpoint.of(instance1).target(ClusterSpec.Id.from("c1"), prodZone).on(Port.tls()).in(SystemName.main),
-
- // Non-default instance in main
- "https://i2.a2.t2.us-north-1.vespa.oath.cloud/",
- Endpoint.of(instance2).target(cluster, prodZone2).on(Port.tls()).in(SystemName.main),
-
- // Non-default cluster in public
- "https://c1.a1.t1.us-north-1.z.vespa-app.cloud/",
- Endpoint.of(instance1).target(ClusterSpec.Id.from("c1"), prodZone).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public),
-
- // Non-default cluster and instance in public
- "https://c2.i2.a2.t2.us-north-1.z.vespa-app.cloud/",
- Endpoint.of(instance2).target(ClusterSpec.Id.from("c2"), prodZone2).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public)
- );
- tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString()));
-
- Map<String, Endpoint> tests2 = Map.of(
- // Non-default cluster and instance in public CD (legacy)
- "https://c2.i2.a2.t2.us-north-1.z.cd.vespa-app.cloud/",
- Endpoint.of(instance2).target(ClusterSpec.Id.from("c2"), prodZone2).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.PublicCd),
-
- // Custom cluster name in public
- "https://c1.a1.t1.us-north-1.z.vespa-app.cloud/",
- Endpoint.of(instance1).target(ClusterSpec.Id.from("c1"), prodZone).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public),
-
- // Default cluster name in non-production zone in public
- "https://a1.t1.us-north-2.test.z.vespa-app.cloud/",
- Endpoint.of(instance1).target(ClusterSpec.Id.from("default"), testZone).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.Public),
-
- // Default cluster name in public CD
- "https://a1.t1.us-north-1.z.cd.vespa-app.cloud/",
- Endpoint.of(instance1).target(ClusterSpec.Id.from("default"), prodZone).on(Port.tls()).routingMethod(RoutingMethod.exclusive).in(SystemName.PublicCd)
- );
- tests2.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString()));
- }
-
- @Test
- void certificate_endpoints() {
- var defaultCluster = ClusterSpec.Id.from("default");
- var prodZone = new DeploymentId(instance1, ZoneId.from("prod", "us-north-1"));
- var testZone = new DeploymentId(instance1, ZoneId.from("test", "us-north-2"));
-
- var tests = Map.of(
- // Default rotation
- "https://a1.t1.g.vespa-app.cloud/",
- Endpoint.of(instance1)
- .target(EndpointId.defaultId())
- .certificateName()
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.Public),
-
- // Wildcard to match other rotations
- "https://*.a1.t1.g.vespa-app.cloud/",
- Endpoint.of(instance1)
- .wildcard()
- .certificateName()
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.Public),
-
- // Default cluster in zone
- "https://a1.t1.us-north-1.z.vespa-app.cloud/",
- Endpoint.of(instance1)
- .target(defaultCluster, prodZone)
- .certificateName()
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.Public),
-
- // Default cluster in test zone
- "https://a1.t1.us-north-2.test.z.vespa-app.cloud/",
- Endpoint.of(instance1)
- .target(defaultCluster, testZone)
- .certificateName()
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.Public),
-
- // Wildcard to match other clusters in test zone
- "https://*.a1.t1.us-north-2.test.z.vespa-app.cloud/",
- Endpoint.of(instance1)
- .wildcard(testZone)
- .certificateName()
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.Public),
-
- // Wildcard to match other clusters in zone
- "https://*.a1.t1.us-north-1.z.vespa-app.cloud/",
- Endpoint.of(instance1)
- .wildcard(prodZone)
- .certificateName()
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.Public)
- );
-
- tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString()));
- }
-
- @Test
- void region_endpoints() {
- var cluster = ClusterSpec.Id.from("default");
- var prodZone = ZoneId.from("prod", "us-north-2");
- Map<String, Endpoint> tests = Map.of(
- "https://a1.t1.aws-us-north-1.w.vespa-app.cloud/",
- Endpoint.of(instance1)
- .targetRegion(cluster, "us-north-1", CloudName.AWS)
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.Public),
- "https://a1.t1.gcp-us-south1.w.vespa-app.cloud/",
- Endpoint.of(instance1)
- .targetRegion(cluster, "us-south1", CloudName.GCP)
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.Public),
- "https://c1.a1.t1.aws-us-north-2.w.vespa-app.cloud/",
- Endpoint.of(instance1)
- .targetRegion(ClusterSpec.Id.from("c1"), "us-north-2", CloudName.AWS)
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.Public),
- "https://deadbeef.cafed00d.aws-us-north-2.w.vespa-app.cloud/",
- Endpoint.of(instance1)
- .targetRegion(ClusterSpec.Id.from("c1"), "us-north-2", CloudName.AWS)
- .routingMethod(RoutingMethod.exclusive)
- .generatedFrom(new GeneratedEndpoint("deadbeef", "cafed00d", AuthMethod.mtls, Optional.empty()))
- .on(Port.tls())
- .in(SystemName.Public),
- "https://c1.a1.t1.aws-us-north-2-w.vespa.oath.cloud/",
- Endpoint.of(instance1)
- .targetRegion(ClusterSpec.Id.from("c1"), "us-north-2", CloudName.AWS)
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.main)
- );
- tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString()));
- }
-
- @Test
- void application_endpoints() {
- Map<String, Endpoint> tests = Map.of(
- "https://weighted.a1.t1.a.vespa-app.cloud/",
- Endpoint.of(app1)
- .targetApplication(EndpointId.of("weighted"), ClusterSpec.Id.from("qrs"),
- Map.of(new DeploymentId(app1.instance("i1"), ZoneId.from("prod", "us-west-1")), 1))
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.Public),
- "https://weighted.a1.t1.a.cd.vespa-app.cloud/",
- Endpoint.of(app1)
- .targetApplication(EndpointId.of("weighted"), ClusterSpec.Id.from("qrs"),
- Map.of(new DeploymentId(app1.instance("i1"), ZoneId.from("prod", "us-west-1")), 1))
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.PublicCd),
- "https://a2.t2.a.vespa.oath.cloud/",
- Endpoint.of(app2)
- .targetApplication(EndpointId.defaultId(), ClusterSpec.Id.from("qrs"),
- Map.of(new DeploymentId(app2.instance("i1"), ZoneId.from("prod", "us-east-3")), 1))
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.main),
- "https://cd.a2.t2.a.cd.vespa.oath.cloud/",
- Endpoint.of(app2)
- .targetApplication(EndpointId.defaultId(), ClusterSpec.Id.from("qrs"),
- Map.of(new DeploymentId(app2.instance("i1"), ZoneId.from("prod", "us-east-3")), 1))
- .routingMethod(RoutingMethod.exclusive)
- .on(Port.tls())
- .in(SystemName.cd)
- );
- tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.url().toString()));
- }
-
- @Test
- void upstream_name() {
- var zone = new DeploymentId(instance1, ZoneId.from("prod", "us-north-1"));
- var zone2 = new DeploymentId(instance2, ZoneId.from("prod", "us-north-1"));
- var tests1 = Map.of(
- // With default cluster
- "a1.t1.us-north-1.prod",
- Endpoint.of(instance1).target(EndpointId.defaultId(), ClusterSpec.Id.from("default"), List.of(zone)).on(Port.tls()).in(SystemName.main),
-
- // With non-default cluster
- "c1.a1.t1.us-north-1.prod",
- Endpoint.of(instance1).target(EndpointId.of("ignored1"), ClusterSpec.Id.from("c1"), List.of(zone)).on(Port.tls()).in(SystemName.main),
-
- // With application endpoint
- "c2.a1.t1.us-north-1.prod",
- Endpoint.of(app1).targetApplication(EndpointId.defaultId(), ClusterSpec.Id.from("c2"), Map.of(new DeploymentId(app1.instance("i1"), zone.zoneId()), 1))
- .routingMethod(RoutingMethod.sharedLayer4)
- .on(Port.tls())
- .in(SystemName.main)
- );
- var tests2 = Map.of(
- // With non-default instance and default cluster
- "i2.a2.t2.us-north-1.prod",
- Endpoint.of(instance2).target(EndpointId.defaultId(), ClusterSpec.Id.from("default"), List.of(zone2)).on(Port.tls()).in(SystemName.main),
-
- // With non-default instance and cluster
- "c2.i2.a2.t2.us-north-1.prod",
- Endpoint.of(instance2).target(EndpointId.of("ignored2"), ClusterSpec.Id.from("c2"), List.of(zone2)).on(Port.tls()).in(SystemName.main)
- );
- tests1.forEach((expected, endpoint) -> assertEquals(expected, endpoint.upstreamName(zone)));
- tests2.forEach((expected, endpoint) -> assertEquals(expected, endpoint.upstreamName(zone2)));
- }
-
- @Test
- public void generated_id() {
- GeneratedEndpoint ge1 = new GeneratedEndpoint("cafed00d", "deadbeef", AuthMethod.mtls, Optional.empty());
- GeneratedEndpoint ge2 = new GeneratedEndpoint("dead2bad", "deadbeef", AuthMethod.mtls, Optional.of(EndpointId.of("foo")));
- var deployment = new DeploymentId(instance1, ZoneId.from("prod", "us-north-1"));
- var tests = Map.of(
- // Zone endpoint in main, unlike named endpoints, this includes the scope symbol 'z'
- "cafed00d.deadbeef.z.vespa.oath.cloud",
- Endpoint.of(instance1).target(ClusterSpec.Id.from("c1"), deployment).generatedFrom(ge1)
- .routingMethod(RoutingMethod.sharedLayer4).on(Port.tls()).in(SystemName.main),
- // Zone endpoint in public
- "cafed00d.deadbeef.z.vespa-app.cloud",
- Endpoint.of(instance1).target(ClusterSpec.Id.from("c1"), deployment).generatedFrom(ge1)
- .routingMethod(RoutingMethod.exclusive).on(Port.tls()).in(SystemName.Public),
- // Global endpoint in public
- "dead2bad.deadbeef.g.vespa-app.cloud",
- Endpoint.of(instance1).target(EndpointId.of("foo"), ClusterSpec.Id.from("c1"), List.of(deployment))
- .generatedFrom(ge2)
- .routingMethod(RoutingMethod.exclusive).on(Port.tls()).in(SystemName.Public),
- // Application endpoint in public
- "dead2bad.deadbeef.a.vespa-app.cloud",
- Endpoint.of(TenantAndApplicationId.from(instance1)).targetApplication(EndpointId.of("foo"), deployment)
- .generatedFrom(ge2)
- .routingMethod(RoutingMethod.exclusive).on(Port.tls()).in(SystemName.Public),
- // Wildcard endpoint for zone
- "*.deadbeef.z.vespa-app.cloud",
- Endpoint.of(instance1)
- .wildcardGenerated(ge1.applicationPart(), Endpoint.Scope.zone)
- .certificateName()
- .routingMethod(RoutingMethod.exclusive).on(Port.tls()).in(SystemName.Public),
- // Wildcard endpoint for global
- "*.deadbeef.g.vespa-app.cloud",
- Endpoint.of(instance1)
- .wildcardGenerated(ge1.applicationPart(), Endpoint.Scope.global)
- .certificateName()
- .routingMethod(RoutingMethod.exclusive).on(Port.tls()).in(SystemName.Public),
- // Wildcard endpoint for application
- "*.deadbeef.a.vespa-app.cloud",
- Endpoint.of(instance1)
- .wildcardGenerated(ge1.applicationPart(), Endpoint.Scope.application)
- .certificateName()
- .routingMethod(RoutingMethod.exclusive).on(Port.tls()).in(SystemName.Public)
- );
- tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.dnsName()));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageDiffTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageDiffTest.java
deleted file mode 100644
index fbbd199aa05..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageDiffTest.java
+++ /dev/null
@@ -1,127 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.Map;
-import java.util.zip.Deflater;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-
-import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageDiff.diff;
-import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageDiff.diffAgainstEmpty;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author freva
- */
-public class ApplicationPackageDiffTest {
- private static final ApplicationPackage app1 = applicationPackage(Map.of("file1", "contents of the\nfirst file", "dir/myfile", "Second file", "dir/binary", "øøøø"));
- private static final ApplicationPackage app2 = applicationPackage(Map.of("file1", "updated contents\nof the\nfirst file\nafter some changes", "dir/myfile2", "Second file", "dir/binary", "øøøø"));
-
- @Test
- void no_diff() {
- assertEquals("No diff\n", new String(diff(app1, app1)));
- }
-
- @Test
- void diff_against_empty() {
- assertEquals("--- dir/binary\n" +
- "Diff skipped: File is binary (new file -> 8B)\n" +
- "\n" +
- "--- dir/myfile\n" +
- "@@ -1,0 +1,1 @@\n" +
- "+ Second file\n" +
- "\n" +
- "--- file1\n" +
- "@@ -1,0 +1,2 @@\n" +
- "+ contents of the\n" +
- "+ first file\n" +
- "\n", new String(diffAgainstEmpty(app1)));
- }
-
- @Test
- void full_diff() {
- // Even though dir/binary is binary file, we can see they are identical, so it should not print "Diff skipped"
- assertEquals("--- dir/myfile\n" +
- "@@ -1,1 +1,0 @@\n" +
- "- Second file\n" +
- "\n" +
- "--- dir/myfile2\n" +
- "@@ -1,0 +1,1 @@\n" +
- "+ Second file\n" +
- "\n" +
- "--- file1\n" +
- "@@ -1,2 +1,4 @@\n" +
- "+ updated contents\n" +
- "+ of the\n" +
- "- contents of the\n" +
- " first file\n" +
- "+ after some changes\n" +
- "\n", new String(diff(app1, app2)));
- }
-
- @Test
- void skips_diff_for_too_large_files() {
- assertEquals("--- dir/myfile\n" +
- "@@ -1,1 +1,0 @@\n" +
- "- Second file\n" +
- "\n" +
- "--- dir/myfile2\n" +
- "@@ -1,0 +1,1 @@\n" +
- "+ Second file\n" +
- "\n" +
- "--- file1\n" +
- "Diff skipped: File too large (26B -> 53B)\n" +
- "\n", new String(diff(app1, app2, 12, 1000, 1000)));
- }
-
- @Test
- void skips_diff_if_file_diff_is_too_large() {
- assertEquals("--- dir/myfile\n" +
- "@@ -1,1 +1,0 @@\n" +
- "- Second file\n" +
- "\n" +
- "--- dir/myfile2\n" +
- "@@ -1,0 +1,1 @@\n" +
- "+ Second file\n" +
- "\n" +
- "--- file1\n" +
- "Diff skipped: Diff too large (96B)\n" +
- "\n", new String(diff(app1, app2, 1000, 50, 1000)));
- }
-
- @Test
- void skips_diff_if_total_diff_is_too_large() {
- assertEquals("--- dir/myfile\n" +
- "@@ -1,1 +1,0 @@\n" +
- "- Second file\n" +
- "\n" +
- "--- dir/myfile2\n" +
- "Diff skipped: Total diff size >20B)\n" +
- "\n" +
- "--- file1\n" +
- "Diff skipped: Total diff size >20B)\n" +
- "\n", new String(diff(app1, app2, 1000, 1000, 20)));
- }
-
- private static ApplicationPackage applicationPackage(Map<String, String> files) {
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- try (ZipOutputStream out = new ZipOutputStream(baos)) {
- out.setLevel(Deflater.NO_COMPRESSION); // This is for testing purposes so we skip compression for performance
- for (Map.Entry<String, String> file : files.entrySet()) {
- ZipEntry entry = new ZipEntry(file.getKey());
- out.putNextEntry(entry);
- out.write(file.getValue().getBytes(UTF_8));
- out.closeEntry();
- }
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- return new ApplicationPackage(baos.toByteArray());
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java
deleted file mode 100644
index 988a20b44ad..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/ApplicationPackageTest.java
+++ /dev/null
@@ -1,310 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.io.LazyInputStream;
-import org.junit.jupiter.api.Test;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.io.SequenceInputStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.function.Predicate;
-import java.util.function.UnaryOperator;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.filesZip;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author valerijf
- * @author jonmv
- */
-public class ApplicationPackageTest {
-
- static final String deploymentXml = """
- <?xml version="1.0" encoding="UTF-8"?>
- <deployment version="1.0">
- <test />
- <prod>
- <parallel>
- <region active="true">us-central-1</region>
- </parallel>
- </prod>
- </deployment>
- """;
-
- static final String servicesXml = """
- <services version='1.0' xmlns:deploy="vespa" xmlns:preprocess="properties">
- <preprocess:include file='jdisc.xml' />
- <content version='1.0' if='foo' />
- <content version='1.0' id='foo' deploy:environment='staging prod' deploy:region='us-east-3 us-central-1'>
- <preprocess:include file='content/content.xml' />
- </content>
- <preprocess:include file='not_found.xml' required='false' />
- </services>
- """;
-
- private static final String jdiscXml = "<container id='stateless' version='1.0' />\n";
-
- private static final String contentXml = """
- <documents>
- <document type="music.sd" mode="index" />
- </documents>
- <preprocess:include file="nodes.xml" />""";
-
- private static final String nodesXml = """
- <nodes>
- <node hostalias="node0" distribution-key="0" />
- </nodes>""";
-
- @Test
- void test_createEmptyForDeploymentRemoval() {
- ApplicationPackage app = ApplicationPackage.deploymentRemoval();
- assertEquals(DeploymentSpec.empty, app.deploymentSpec());
-
- for (ValidationId validationId : ValidationId.values()) {
- assertTrue(app.validationOverrides().allows(validationId, Instant.now()));
- }
- }
-
- @Test
- void testMetaData() {
- byte[] zip = filesZip(Map.of("services.xml", servicesXml.getBytes(UTF_8),
- "jdisc.xml", jdiscXml.getBytes(UTF_8),
- "content/content.xml", contentXml.getBytes(UTF_8),
- "content/nodes.xml", nodesXml.getBytes(UTF_8),
- "gurba", "gurba".getBytes(UTF_8)));
-
- assertEquals(Map.of("services.xml", servicesXml,
- "jdisc.xml", jdiscXml,
- "content/content.xml", contentXml,
- "content/nodes.xml", nodesXml),
- unzip(new ApplicationPackage(zip).metaDataZip()));
- }
-
- @Test
- void testMetaDataWithMissingFiles() {
- byte[] zip = filesZip(Map.of("services.xml", servicesXml.getBytes(UTF_8)));
-
- try {
- new ApplicationPackage(zip).metaDataZip();
- fail("Should fail on missing include file");
- }
- catch (RuntimeException e) {
- assertEquals("./jdisc.xml", e.getCause().getMessage());
- }
- }
-
- @Test
- void testAbsoluteInclude() throws Exception {
- try {
- getApplicationZip("include-absolute.zip");
- fail("Should fail on include file outside zip");
- }
- catch (RuntimeException e) {
- assertEquals(IllegalArgumentException.class, e.getClass());
- }
- }
-
- @Test
- void testParentInclude() throws Exception {
- try {
- getApplicationZip("include-parent.zip");
- fail("Should fail on include file outside zip");
- }
- catch (RuntimeException e) {
- assertEquals("./../not_found.xml is not a descendant of .", e.getMessage());
- }
- }
-
- @Test
- void testBundleHashesAreSameWithDifferentDeploymentXml() throws Exception {
- var originalPackage = getApplicationZip("original.zip");
- var changedServices = getApplicationZip("changed-services-xml.zip");
- var changedDeploymentXml = getApplicationZip("changed-deployment-xml.zip");
- var similarDeploymentXml = getApplicationZip("similar-deployment-xml.zip");
-
- // services.xml is changed -> different bundle hash
- assertNotEquals(originalPackage.bundleHash(), changedServices.bundleHash());
-
- // deployment.xml is changed, with real changes -> different bundle hash
- assertNotEquals(originalPackage.bundleHash(), changedDeploymentXml.bundleHash());
-
- // deployment.xml is changed, but only deployment orchestration settings -> same bundle hash
- assertEquals(originalPackage.bundleHash(), similarDeploymentXml.bundleHash());
- }
-
- @Test
- void testCertificateFileExists() throws Exception {
- getApplicationZip("with-certificate.zip", true);
- }
-
- @Test
- void testCertificateFileMissing() throws Exception {
- try {
- getApplicationZip("original.zip", true);
- fail("Should fail on missing certificate file file");
- } catch (RuntimeException e) {
- assertEquals("No client certificate found in security/ in application package, see https://cloud.vespa.ai/en/security/guide", e.getMessage());
- }
- }
-
- public static Map<String, String> unzip(byte[] zip) {
- return ZipEntries.from(zip, __ -> true, 1 << 24, true)
- .asList().stream()
- .collect(Collectors.toMap(ZipEntries.ZipEntryWithContent::name,
- entry -> new String(entry.content().orElse(new byte[0]), UTF_8)));
- }
-
- private ApplicationPackage getApplicationZip(String path) throws IOException {
- return getApplicationZip(path, false);
- }
-
- private ApplicationPackage getApplicationZip(String path, boolean checkCertificateFile) throws IOException {
- return new ApplicationPackage(Files.readAllBytes(Path.of("src/test/resources/application-packages/" + path)), true, checkCertificateFile);
- }
-
- public static byte[] zip(Map<String, String> content) {
- return filesZip(content.entrySet().stream().collect(Collectors.toMap(entry -> entry.getKey(),
- entry -> entry.getValue().getBytes(UTF_8))));
- }
-
- private static class AngryStreams {
-
- private final byte[] content;
- private final Map<ByteArrayInputStream, Throwable> streams = new LinkedHashMap<>();
-
- AngryStreams(byte[] content) {
- this.content = content;
- }
-
- InputStream stream() {
- ByteArrayInputStream stream = new ByteArrayInputStream(Arrays.copyOf(content, content.length)) {
- boolean closed = false;
- @Override public void close() { closed = true; }
- @Override public int read() { assertFalse(closed); return super.read(); }
- @Override public int read(byte[] b, int off, int len) { assertFalse(closed); return super.read(b, off, len); }
- @Override public long transferTo(OutputStream out) throws IOException { assertFalse(closed); return super.transferTo(out); }
- @Override public byte[] readAllBytes() { assertFalse(closed); return super.readAllBytes(); }
- };
- streams.put(stream, new Throwable());
- return stream;
- }
-
- void verifyAllRead() {
- streams.forEach((stream, stack) -> assertEquals(0, stream.available(),
- "unconsumed content in stream created at " +
- new ByteArrayOutputStream() {{ stack.printStackTrace(new PrintStream(this)); }}));
- }
-
- }
-
- @Test
- void testApplicationPackageStream() throws Exception {
- Map<String, String> content = Map.of("deployment.xml", deploymentXml,
- "services.xml", servicesXml,
- "jdisc.xml", jdiscXml,
- "unused1.xml", jdiscXml,
- "content/content.xml", contentXml,
- "content/nodes.xml", nodesXml,
- "gurba", "gurba");
- byte[] zip = zip(content);
- assertEquals(content, unzip(zip));
- AngryStreams angry = new AngryStreams(zip);
-
- ApplicationPackageStream identity = new ApplicationPackageStream(angry::stream, () -> __ -> true);
- InputStream lazy = new LazyInputStream(() -> new ByteArrayInputStream(identity.truncatedPackage().zippedContent()));
- assertEquals("must completely exhaust input before reading package",
- assertThrows(IllegalStateException.class, identity::truncatedPackage).getMessage());
-
- // Verify no content has changed when passing through the stream.
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- try (InputStream stream = identity.zipStream()) { stream.transferTo(out); }
- assertEquals(content, unzip(out.toByteArray()));
- assertEquals(content, unzip(identity.truncatedPackage().zippedContent()));
- assertEquals(content, unzip(lazy.readAllBytes()));
- ApplicationPackage original = new ApplicationPackage(zip);
- assertEquals(unzip(original.metaDataZip()), unzip(identity.truncatedPackage().metaDataZip()));
- assertEquals(original.bundleHash(), identity.truncatedPackage().bundleHash());
-
- // Change deployment.xml, remove unused1.xml and add unused2.xml
- Map<String, UnaryOperator<InputStream>> replacements = Map.of("deployment.xml", in -> new SequenceInputStream(in, new ByteArrayInputStream("\n\n".getBytes(UTF_8))),
- "unused1.xml", in -> null,
- "unused2.xml", __ -> new ByteArrayInputStream(jdiscXml.getBytes(UTF_8)));
- Predicate<String> truncation = name -> name.endsWith(".xml");
- ApplicationPackageStream modifier = new ApplicationPackageStream(angry::stream, () -> truncation, replacements);
- out.reset();
-
- InputStream partiallyRead = modifier.zipStream();
- assertEquals(15, partiallyRead.readNBytes(15).length);
-
- try (InputStream stream = modifier.zipStream()) { stream.transferTo(out); }
-
- assertEquals(Map.of("deployment.xml", deploymentXml + "\n\n",
- "services.xml", servicesXml,
- "jdisc.xml", jdiscXml,
- "unused2.xml", jdiscXml,
- "content/content.xml", contentXml,
- "content/nodes.xml", nodesXml,
- "gurba", "gurba"),
- unzip(out.toByteArray()));
-
- assertEquals(Map.of("deployment.xml", deploymentXml + "\n\n",
- "services.xml", servicesXml,
- "jdisc.xml", jdiscXml,
- "unused2.xml", jdiscXml,
- "content/content.xml", contentXml,
- "content/nodes.xml", nodesXml),
- unzip(modifier.truncatedPackage().zippedContent()));
-
- // Compare retained metadata for an updated original package, and the truncated package of the modifier.
- assertEquals(unzip(new ApplicationPackage(zip(Map.of("deployment.xml", deploymentXml + "\n\n", // Expected to change.
- "services.xml", servicesXml,
- "jdisc.xml", jdiscXml,
- "unused1.xml", jdiscXml, // Irrelevant.
- "content/content.xml", contentXml,
- "content/nodes.xml", nodesXml,
- "gurba", "gurba"))).metaDataZip()),
- unzip(modifier.truncatedPackage().metaDataZip()));
-
- try (InputStream stream1 = modifier.zipStream();
- InputStream stream2 = modifier.zipStream()) {
- assertArrayEquals(stream1.readAllBytes(),
- stream2.readAllBytes());
- }
-
- ByteArrayOutputStream byteAtATime = new ByteArrayOutputStream();
- try (InputStream stream1 = modifier.zipStream();
- InputStream stream2 = modifier.zipStream()) {
- for (int b; (b = stream1.read()) != -1; ) byteAtATime.write(b);
- assertArrayEquals(stream2.readAllBytes(),
- byteAtATime.toByteArray());
- }
-
- assertEquals(byteAtATime.size(),
- 15 + partiallyRead.readAllBytes().length);
- partiallyRead.close();
-
- try (InputStream stream = modifier.zipStream()) { stream.readNBytes(12); }
-
- angry.verifyAllRead();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/BasicServicesXmlTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/BasicServicesXmlTest.java
deleted file mode 100644
index ff103ddddfa..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/BasicServicesXmlTest.java
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import com.yahoo.text.XML;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.application.pkg.BasicServicesXml.Container;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-class BasicServicesXmlTest {
-
- @Test
- public void parse() {
- assertServices(new BasicServicesXml(List.of()), "<services/>");
- assertServices(new BasicServicesXml(List.of(new Container("foo", List.of(Container.AuthMethod.mtls), List.of()),
- new Container("bar", List.of(Container.AuthMethod.mtls), List.of()),
- new Container("container", List.of(Container.AuthMethod.mtls), List.of()))),
- """
- <services>
- <container id="foo"/>
- <container id="bar"/>
- <container/>
- </services>
- """);
- assertServices(new BasicServicesXml(List.of(
- new Container("foo",
- List.of(Container.AuthMethod.mtls,
- Container.AuthMethod.token),
- List.of(TokenId.of("my-token"),
- TokenId.of("other-token"))),
- new Container("bar", List.of(Container.AuthMethod.mtls), List.of()))),
- """
- <services>
- <container id="foo">
- <clients>
- <client id="mtls"/>
- <client id="token">
- <token id="my-token"/>
- </client>
- <client id="token2">
- <token id="other-token"/>
- </client>
- </clients>
- </container>
- <container id="bar"/>
- </services>
- """);
- }
-
- private void assertServices(BasicServicesXml expected, String xmlForm) {
- assertEquals(expected, BasicServicesXml.parse(XML.getDocument(xmlForm)));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/LinesComparatorTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/LinesComparatorTest.java
deleted file mode 100644
index 92506459728..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/LinesComparatorTest.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import org.junit.jupiter.api.Test;
-
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class LinesComparatorTest {
- private static final String text1 = "This part of the\n" +
- "document has stayed the\n" +
- "same from version to\n" +
- "version. It shouldn't\n" +
- "be shown if it doesn't\n" +
- "change. Otherwise, that\n" +
- "would not be helping to\n" +
- "compress the size of the\n" +
- "changes.\n" +
- "\n" +
- "This paragraph contains\n" +
- "text that is outdated.\n" +
- "It will be deleted in the\n" +
- "near future.\n" +
- "\n" +
- "It is important to spell\n" +
- "check this dokument. On\n" +
- "the other hand, a\n" +
- "misspelled word isn't\n" +
- "the end of the world.\n" +
- "Nothing in the rest of\n" +
- "this paragraph needs to\n" +
- "be changed. Things can\n" +
- "be added after it.";
- private static final String text2 = "This is an important\n" +
- "notice! It should\n" +
- "therefore be located at\n" +
- "the beginning of this\n" +
- "document!\n" +
- "\n" +
- "This part of the\n" +
- "document has stayed the\n" +
- "same from version to\n" +
- "version. It shouldn't\n" +
- "be shown if it doesn't\n" +
- "change. Otherwise, that\n" +
- "would not be helping to\n" +
- "compress the size of the\n" +
- "changes.\n" +
- "\n" +
- "It is important to spell\n" +
- "check this document. On\n" +
- "the other hand, a\n" +
- "misspelled word isn't\n" +
- "the end of the world.\n" +
- "Nothing in the rest of\n" +
- "this paragraph needs to\n" +
- "be changed. Things can\n" +
- "be added after it.\n" +
- "\n" +
- "This paragraph contains\n" +
- "important new additions\n" +
- "to this document.";
-
- @Test
- void diff_test() {
- assertDiff(null, "", "");
- assertDiff(null, text1, text1);
- assertDiff(text1.lines().map(line -> "- " + line).collect(Collectors.joining("\n", "@@ -1,24 +1,0 @@\n", "\n")), text1, "");
- assertDiff(text1.lines().map(line -> "+ " + line).collect(Collectors.joining("\n", "@@ -1,0 +1,24 @@\n", "\n")), "", text1);
- assertDiff("@@ -1,3 +1,9 @@\n" +
- "+ This is an important\n" +
- "+ notice! It should\n" +
- "+ therefore be located at\n" +
- "+ the beginning of this\n" +
- "+ document!\n" +
- "+ \n" +
- " This part of the\n" +
- " document has stayed the\n" +
- " same from version to\n" +
- "@@ -7,14 +13,9 @@\n" +
- " would not be helping to\n" +
- " compress the size of the\n" +
- " changes.\n" +
- "- \n" +
- "- This paragraph contains\n" +
- "- text that is outdated.\n" +
- "- It will be deleted in the\n" +
- "- near future.\n" +
- " \n" +
- " It is important to spell\n" +
- "+ check this document. On\n" +
- "- check this dokument. On\n" +
- " the other hand, a\n" +
- " misspelled word isn't\n" +
- " the end of the world.\n" +
- "@@ -22,3 +23,7 @@\n" +
- " this paragraph needs to\n" +
- " be changed. Things can\n" +
- " be added after it.\n" +
- "+ \n" +
- "+ This paragraph contains\n" +
- "+ important new additions\n" +
- "+ to this document.\n", text1, text2);
- }
-
- private static void assertDiff(String expected, String left, String right) {
- assertEquals(Optional.ofNullable(expected),
- LinesComparator.diff(left.lines().toList(), right.lines().toList()));
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java
deleted file mode 100644
index 3cc05df0953..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/pkg/TestPackageTest.java
+++ /dev/null
@@ -1,288 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.application.pkg;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
-import com.yahoo.vespa.hosted.controller.application.pkg.TestPackage.TestSummary;
-import com.yahoo.vespa.hosted.controller.config.ControllerConfig;
-import com.yahoo.vespa.hosted.controller.config.ControllerConfig.Steprunner.Testerapp;
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.jar.JarOutputStream;
-import java.util.zip.ZipEntry;
-
-import static com.yahoo.config.provision.CloudName.AWS;
-import static com.yahoo.config.provision.CloudName.DEFAULT;
-import static com.yahoo.config.provision.CloudName.GCP;
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.production;
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging;
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.staging_setup;
-import static com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Suite.system;
-import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage.deploymentFile;
-import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageTest.unzip;
-import static com.yahoo.vespa.hosted.controller.application.pkg.TestPackage.deploymentXml;
-import static com.yahoo.vespa.hosted.controller.application.pkg.TestPackage.validateTests;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author jonmv
- */
-public class TestPackageTest {
-
- static byte[] testsJar(String... suites) throws IOException {
- String manifest = "Manifest-Version: 1.0\n" +
- "Created-By: vespa container maven plugin\n" +
- "Build-Jdk-Spec: 17\n" +
- "Bundle-ManifestVersion: 2\n" +
- "Bundle-SymbolicName: canary-application-test\n" +
- "Bundle-Version: 1.0.1\n" +
- "Bundle-Name: Test & verification application for Vespa\n" +
- "X-JDisc-Test-Bundle-Version: 1.0\n" +
- "Bundle-Vendor: Yahoo!\n" +
- "Bundle-ClassPath: .,dependencies/fest-assert-1.4.jar,dependencies/fest-u\n" +
- " til-1.1.6.jar\n" +
- "Import-Package: ai.vespa.feed.client;version=\"[1.0.0,2)\",ai.vespa.hosted\n" +
- " .cd;version=\"[1.0.0,2)\",com.yahoo.config;version=\"[1.0.0,2)\",com.yahoo.\n" +
- " container.jdisc;version=\"[1.0.0,2)\",com.yahoo.jdisc.http;version=\"[1.0.\n" +
- " 0,2)\",com.yahoo.slime;version=\"[1.0.0,2)\",java.awt.image;version=\"[0.0.\n" +
- " 0,1)\",java.awt;version=\"[0.0.0,1)\",java.beans;version=\"[0.0.0,1)\",java.\n" +
- " io;version=\"[0.0.0,1)\",java.lang.annotation;version=\"[0.0.0,1)\",java.la\n" +
- " ng.reflect;version=\"[0.0.0,1)\",java.lang;version=\"[0.0.0,1)\",java.math;\n" +
- " version=\"[0.0.0,1)\",java.net.http;version=\"[0.0.0,1)\",java.net;version=\n" +
- " \"[0.0.0,1)\",java.nio.file;version=\"[0.0.0,1)\",java.security;version=\"[0\n" +
- " .0.0,1)\",java.text;version=\"[0.0.0,1)\",java.time.temporal;version=\"[0.0\n" +
- " .0,1)\",java.time;version=\"[0.0.0,1)\",java.util.concurrent;version=\"[0.0\n" +
- " .0,1)\",java.util.function;version=\"[0.0.0,1)\",java.util.stream;version=\n" +
- " \"[0.0.0,1)\",java.util;version=\"[0.0.0,1)\",javax.imageio;version=\"[0.0.0\n" +
- " ,1)\",org.junit.jupiter.api;version=\"[5.8.1,6)\"\n" +
- "X-JDisc-Test-Bundle-Categories: " + String.join(",", suites) + "\n" +
- "\n";
-
- ByteArrayOutputStream buffer = new ByteArrayOutputStream();
- try (JarOutputStream out = new JarOutputStream(buffer)) {
- write("META-INF/MANIFEST.MF", manifest, out);
- write("dependencies/foo.jar", "bar", out);
- write("META-INF/maven/ai.vespa.test/app/pom.xml", "<project />", out);
- write("ai/vespa/test/Test.class", "baz", out);
- }
- return buffer.toByteArray();
- }
-
- static void write(String name, String content, JarOutputStream out) throws IOException {
- out.putNextEntry(new ZipEntry(name));
- out.write(content.getBytes(UTF_8));
- out.closeEntry();
- }
-
- @Test
- void testBundleValidation() throws IOException {
- byte[] testZip = ApplicationPackage.filesZip(Map.of("components/foo-tests.jar", testsJar("SystemTest", "StagingSetup", "ProductionTest"),
- "artifacts/key", new byte[0]));
- TestSummary summary = validateTests(List.of(system), testZip);
-
- assertEquals(List.of(system, staging_setup, production), summary.suites());
- assertEquals(List.of("test package contains 'artifacts/key'; this conflicts with credentials used to run tests in Vespa Cloud",
- "test package has staging setup, so it should also include staging tests",
- "test package has production tests, but no production tests are declared in deployment.xml",
- "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"),
- summary.problems());
- }
-
- @Test
- void testFatTestsValidation() {
- byte[] testZip = ApplicationPackage.filesZip(Map.of("artifacts/foo-tests.jar", new byte[0]));
- TestSummary summary = validateTests(List.of(staging, production), testZip);
-
- assertEquals(List.of(staging, production), summary.suites());
- assertEquals(List.of("test package has staging tests, so it should also include staging setup",
- "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"),
- summary.problems());
- }
-
- @Test
- void testBasicTestsValidation() {
- byte[] testZip = ApplicationPackage.filesZip(Map.of("tests/staging-test/foo.json", new byte[0],
- "tests/staging-setup/foo.json", new byte[0]));
- TestSummary summary = validateTests(List.of(system, production), testZip);
- assertEquals(List.of(staging_setup, staging), summary.suites());
- assertEquals(List.of("test package has no system tests, but <test /> is declared in deployment.xml",
- "test package has no production tests, but production tests are declared in deployment.xml",
- "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"),
- summary.problems());
- }
-
- @Test
- void testTestPackageAssemblyWithFallbackSpec() throws IOException {
- String deploymentSpec = """
- <deployment>
- <test />
- </deployment>
- """;
- byte[] bundleZip = ApplicationPackage.filesZip(Map.of("components/foo-tests.jar", testsJar("SystemTest", "ProductionTest"),
- "artifacts/key", new byte[0]));
-
- TestPackage bundleTests = new TestPackage(() -> new ByteArrayInputStream(bundleZip),
- false,
- CloudName.DEFAULT,
- new RunId(ApplicationId.defaultId(), JobType.dev("abc"), 123),
- new Testerapp.Builder().tenantCdBundle("foo").runtimeProviderClass("bar").build(),
- DeploymentSpec.fromXml(deploymentSpec),
- null,
- null);
-
- Map<String, String> bundlePackage = unzip(bundleTests.asApplicationPackage().zipStream().readAllBytes());
- bundlePackage.keySet().removeIf(name -> name.startsWith("tests/.ignore") || name.startsWith("artifacts/.ignore"));
- assertEquals(Set.of("deployment.xml",
- "services.xml",
- "components/foo-tests.jar",
- "artifacts/key"),
- bundlePackage.keySet());
- assertEquals(Set.of("deployment.xml", "services.xml"),
- unzip(bundleTests.asApplicationPackage().truncatedPackage().zippedContent()).keySet());
- }
- @Test
- void testTestPackageAssembly() throws IOException {
- String deploymentSpec = """
- <deployment>
- <test />
- </deployment>
- """;
- byte[] bundleZip = ApplicationPackage.filesZip(Map.of("components/foo-tests.jar", testsJar("SystemTest", "ProductionTest"),
- "artifacts/key", new byte[0],
- deploymentFile, deploymentSpec.getBytes(UTF_8)));
-
- TestPackage bundleTests = new TestPackage(() -> new ByteArrayInputStream(bundleZip),
- false,
- CloudName.DEFAULT,
- new RunId(ApplicationId.defaultId(), JobType.dev("abc"), 123),
- new Testerapp.Builder().tenantCdBundle("foo").runtimeProviderClass("bar").build(),
- DeploymentSpec.empty, // Will fail, unless contained spec is used.
- null,
- null);
-
- Map<String, String> bundlePackage = unzip(bundleTests.asApplicationPackage().zipStream().readAllBytes());
- bundlePackage.keySet().removeIf(name -> name.startsWith("tests/.ignore") || name.startsWith("artifacts/.ignore"));
- assertEquals(Set.of("deployment.xml",
- "services.xml",
- "components/foo-tests.jar",
- "artifacts/key"),
- bundlePackage.keySet());
- assertEquals(Set.of("deployment.xml", "services.xml"),
- unzip(bundleTests.asApplicationPackage().truncatedPackage().zippedContent()).keySet());
- }
-
- @Test
- void generates_correct_deployment_spec() {
- DeploymentSpec spec = DeploymentSpec.fromXml("""
- <deployment version='1.0' athenz-domain='domain' athenz-service='service' cloud-account='123123123123,gcp:foobar' empty-host-ttl='1h'>
- <test empty-host-ttl='1d' />
- <staging cloud-account='aws:321321321321'/>
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- <region>us-west-1</region>
- <test empty-host-ttl='0m'>us-west-1</test>
- <region empty-host-ttl='1d'>us-central-1</region>
- <test>us-central-1</test>
- </prod>
- </deployment>
- """);
-
- verifyAttributes("", 0, DEFAULT, ZoneId.from("test", "us-east-1"), spec);
- verifyAttributes("", 0, DEFAULT, ZoneId.from("staging", "us-east-2"), spec);
- verifyAttributes("", 0, DEFAULT, ZoneId.from("prod", "us-east-3"), spec);
- verifyAttributes("", 0, DEFAULT, ZoneId.from("prod", "us-west-1"), spec);
- verifyAttributes("", 0, DEFAULT, ZoneId.from("prod", "us-central-1"), spec);
-
- verifyAttributes("aws:123123123123", 1440, AWS, ZoneId.from("test", "us-east-1"), spec);
- verifyAttributes("aws:321321321321", 60, AWS, ZoneId.from("staging", "us-east-2"), spec);
- verifyAttributes("aws:123123123123", 60, AWS, ZoneId.from("prod", "us-east-3"), spec);
- verifyAttributes("aws:123123123123", 0, AWS, ZoneId.from("prod", "us-west-1"), spec);
- verifyAttributes("aws:123123123123", 60, AWS, ZoneId.from("prod", "us-central-1"), spec);
-
- verifyAttributes("gcp:foobar", 1440, GCP, ZoneId.from("test", "us-east-1"), spec);
- verifyAttributes("", 0, GCP, ZoneId.from("staging", "us-east-2"), spec);
- verifyAttributes("gcp:foobar", 60, GCP, ZoneId.from("prod", "us-east-3"), spec);
- verifyAttributes("gcp:foobar", 0, GCP, ZoneId.from("prod", "us-west-1"), spec);
- verifyAttributes("gcp:foobar", 60, GCP, ZoneId.from("prod", "us-central-1"), spec);
- }
-
- private void verifyAttributes(String expectedAccount, int expectedTTL, CloudName cloud, ZoneId zone, DeploymentSpec spec) {
- assertEquals("<?xml version='1.0' encoding='UTF-8'?>\n" +
- "<deployment version='1.0' athenz-domain='domain' athenz-service='service'" +
- (expectedAccount.isEmpty() ? "" : " cloud-account='" + expectedAccount + "' empty-host-ttl='" + expectedTTL + "m'") + "> " +
- "<instance id='default-t' /></deployment>",
- new String(TestPackage.deploymentXml(TesterId.of(ApplicationId.defaultId()), InstanceName.defaultName(), cloud, zone, spec)));
- }
-
- @Test
- void generates_correct_tester_flavor() {
- DeploymentSpec spec = DeploymentSpec.fromXml("""
- <deployment version='1.0' athenz-domain='domain' athenz-service='service'>
- <instance id='first'>
- <test tester-flavor="d-6-16-100" />
- <prod>
- <region active="true">gcp-us-west-1</region>
- <test>gcp-us-west-1</test>
- </prod>
- </instance>
- <instance id='second'>
- <test />
- <staging />
- <prod tester-flavor="d-6-16-100">
- <parallel>
- <region active="true">us-east-3</region>
- <region active="true">us-central-1</region>
- </parallel>
- <region active="true">us-west-1</region>
- <test>us-west-1</test>
- </prod>
- </instance>
- </deployment>
- """);
-
- NodeResources firstResources = TestPackage.testerResourcesFor(ZoneId.from("prod", "gcp-us-west-1"), spec.requireInstance("first"), true);
- assertEquals(TestPackage.DEFAULT_TESTER_RESOURCES_CLOUD.with(NodeResources.Architecture.x86_64), firstResources);
-
- NodeResources secondResources = TestPackage.testerResourcesFor(ZoneId.from("prod", "us-west-1"), spec.requireInstance("second"), false);
- assertEquals(6, secondResources.vcpu(), 1e-9);
- assertEquals(16, secondResources.memoryGb(), 1e-9);
- assertEquals(100, secondResources.diskGb(), 1e-9);
- }
-
- @Test
- void generates_correct_services_xml() throws IOException {
- assertEquals(Files.readString(Paths.get("src/test/resources/test_runner_services.xml-cd")),
- new String(TestPackage.servicesXml(true,
- false,
- false,
- new NodeResources(2, 12, 75, 1, NodeResources.DiskSpeed.fast, NodeResources.StorageType.local),
- new ControllerConfig.Steprunner.Testerapp.Builder().build()),
- UTF_8));
-
- assertEquals(Files.readString(Paths.get("src/test/resources/test_runner_services_with_legacy_tests.xml-cd")),
- new String(TestPackage.servicesXml(true,
- false,
- true,
- new NodeResources(2, 12, 75, 1, NodeResources.DiskSpeed.fast, NodeResources.StorageType.local),
- new ControllerConfig.Steprunner.Testerapp.Builder().build()),
- UTF_8));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/response/application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/response/application.json
deleted file mode 100644
index 37da498b6ec..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/response/application.json
+++ /dev/null
@@ -1,189 +0,0 @@
-{
- "url": "...",
- "id": "vespa.album-recommendation.default",
- "clusters": {
- "default": {
- "type": "container",
- "min": {
- "nodes": 2,
- "groups": 1,
- "resources": {
- "vcpu": 0.0,
- "memoryGb": 0.0,
- "diskGb": 0.0,
- "bandwidthGbps": 0.0,
- "diskSpeed": "fast",
- "storageType": "any",
- "architecture": "x86_64"
- }
- },
- "max": {
- "nodes": 2,
- "groups": 1,
- "resources": {
- "vcpu": 0.0,
- "memoryGb": 0.0,
- "diskGb": 0.0,
- "bandwidthGbps": 0.0,
- "diskSpeed": "fast",
- "storageType": "any",
- "architecture": "x86_64"
- }
- },
- "current": {
- "nodes": 2,
- "groups": 1,
- "resources": {
- "vcpu": 2.0,
- "memoryGb": 8.0,
- "diskGb": 50.0,
- "bandwidthGbps": 0.3,
- "diskSpeed": "fast",
- "storageType": "remote",
- "architecture": "x86_64"
- }
- },
- "suggested": {
- "nodes": 2,
- "groups": 1,
- "resources": {
- "vcpu": 2.0,
- "memoryGb": 8.0,
- "diskGb": 75.0,
- "bandwidthGbps": 0.3,
- "diskSpeed": "fast",
- "storageType": "local",
- "architecture": "x86_64"
- }
- },
- "scalingDuration": 400000
- },
- "logserver": {
- "type": "admin",
- "min": {
- "nodes": 1,
- "groups": 1,
- "resources": {
- "vcpu": 0.0,
- "memoryGb": 0.0,
- "diskGb": 0.0,
- "bandwidthGbps": 0.0,
- "diskSpeed": "fast",
- "storageType": "any",
- "architecture": "x86_64"
- }
- },
- "max": {
- "nodes": 1,
- "groups": 1,
- "resources": {
- "vcpu": 0.0,
- "memoryGb": 0.0,
- "diskGb": 0.0,
- "bandwidthGbps": 0.0,
- "diskSpeed": "fast",
- "storageType": "any",
- "architecture": "x86_64"
- }
- },
- "current": {
- "nodes": 1,
- "groups": 1,
- "resources": {
- "vcpu": 0.5,
- "memoryGb": 4.0,
- "diskGb": 50.0,
- "bandwidthGbps": 0.3,
- "diskSpeed": "fast",
- "storageType": "remote",
- "architecture": "x86_64"
- }
- },
- "suggested": {
- "status" : "unavailable",
- "description" : "",
- "resources" : {
- "nodes": 2,
- "groups": 1,
- "resources": {
- "vcpu": 2.0,
- "memoryGb": 4.0,
- "diskGb": 50.0,
- "bandwidthGbps": 0.3,
- "diskSpeed": "fast",
- "storageType": "local",
- "architecture": "x86_64"
- },
- "at" : 123,
- "peak" : {
- "cpu" : 0.1,
- "memory" : 0.2,
- "disk" : 0.3
- },
- "ideal" : {
- "cpu" : 0.4,
- "memory" : 0.5,
- "disk" : 0.6
- }
- }
- },
- "scalingDuration": 90000
- },
- "music": {
- "type": "content",
- "min": {
- "nodes": 2,
- "groups": 1,
- "resources": {
- "vcpu": 0.0,
- "memoryGb": 0.0,
- "diskGb": 0.0,
- "bandwidthGbps": 0.0,
- "diskSpeed": "fast",
- "storageType": "any",
- "architecture": "x86_64"
- }
- },
- "max": {
- "nodes": 2,
- "groups": 1,
- "resources": {
- "vcpu": 0.0,
- "memoryGb": 0.0,
- "diskGb": 0.0,
- "bandwidthGbps": 0.0,
- "diskSpeed": "fast",
- "storageType": "any",
- "architecture": "x86_64"
- }
- },
- "current": {
- "nodes": 2,
- "groups": 1,
- "resources": {
- "vcpu": 2.0,
- "memoryGb": 8.0,
- "diskGb": 50.0,
- "bandwidthGbps": 0.3,
- "diskSpeed": "fast",
- "storageType": "remote",
- "architecture": "x86_64"
- }
- },
- "suggested": {
- "nodes": 2,
- "groups": 1,
- "resources": {
- "vcpu": 2.0,
- "memoryGb": 4.0,
- "diskGb": 50.0,
- "bandwidthGbps": 0.3,
- "diskSpeed": "fast",
- "storageType": "local",
- "architecture": "x86_64"
- }
- },
- "scalingDuration": 1000000
- }
- }
-} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDbTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDbTest.java
deleted file mode 100644
index 7f8aa592cdb..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/archive/CuratorArchiveBucketDbTest.java
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.archive;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.test.ManualClock;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.MockArchiveService;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.VespaManagedArchiveBucket;
-import org.apache.curator.shaded.com.google.common.collect.Streams;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.time.Duration;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-import java.util.stream.Stream;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class CuratorArchiveBucketDbTest {
-
- @Test
- void archiveUriForTenant() {
- ControllerTester tester = new ControllerTester(SystemName.Public);
- CuratorArchiveBucketDb bucketDb = new CuratorArchiveBucketDb(tester.controller());
-
- tester.curator().writeArchiveBuckets(ZoneId.defaultId(),
- ArchiveBuckets.EMPTY.with(new VespaManagedArchiveBucket("existingBucket", "keyArn").withTenant(TenantName.defaultName())));
-
- // Finds existing bucket in db
- assertEquals(Optional.of(URI.create("s3://existingBucket/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.defaultName(), true));
-
- // Assigns to existing bucket while there is space
- IntStream.range(0, 4).forEach(i ->
- assertEquals(
- Optional.of(URI.create("s3://existingBucket/")), bucketDb
- .archiveUriFor(ZoneId.defaultId(), TenantName.from("tenant" + i), true)));
-
- // Creates new bucket when existing buckets are full
- assertEquals(Optional.of(URI.create("s3://bucketName/")), bucketDb.archiveUriFor(ZoneId.defaultId(), TenantName.from("lastDrop"), true));
-
- // Creates new bucket when there are no existing buckets in zone
- assertEquals(Optional.of(URI.create("s3://bucketName/")), bucketDb.archiveUriFor(ZoneId.from("prod.us-east-3"), TenantName.from("firstInZone"), true));
-
- // Does not create bucket if not required
- assertEquals(Optional.empty(), bucketDb.archiveUriFor(ZoneId.from("prod.us-east-3"), TenantName.from("newTenant"), false));
-
- // Lists all buckets by zone
- Set<TenantName> existingBucketTenants = Streams.concat(Stream.of(TenantName.defaultName()), IntStream.range(0, 4).mapToObj(i -> TenantName.from("tenant" + i))).collect(Collectors.toUnmodifiableSet());
- assertEquals(
- Set.of(
- new VespaManagedArchiveBucket("existingBucket", "keyArn").withTenants(existingBucketTenants),
- new VespaManagedArchiveBucket("bucketName", "keyArn").withTenant(TenantName.from("lastDrop"))),
- bucketDb.buckets(ZoneId.defaultId()).vespaManaged());
- assertEquals(
- Set.of(new VespaManagedArchiveBucket("bucketName", "keyArn").withTenant(TenantName.from("firstInZone"))),
- bucketDb.buckets(ZoneId.from("prod.us-east-3")).vespaManaged());
- }
-
- @Test
- void archiveUriForAccount() {
- Controller controller = new ControllerTester(SystemName.Public).controller();
- CuratorArchiveBucketDb bucketDb = new CuratorArchiveBucketDb(controller);
- MockArchiveService service = (MockArchiveService) controller.serviceRegistry().archiveService();
- ManualClock clock = (ManualClock) controller.clock();
-
- CloudAccount acc1 = CloudAccount.from("001122334455");
- ZoneId z1 = ZoneId.from("prod.us-east-3");
-
- assertEquals(Optional.empty(), bucketDb.archiveUriFor(z1, acc1, true)); // Initially not set
- service.setEnclaveArchiveBucket(z1, acc1, "bucket-1");
- assertEquals(Optional.empty(), bucketDb.archiveUriFor(z1, acc1, false));
- assertEquals(Optional.of(URI.create("s3://bucket-1/")), bucketDb.archiveUriFor(z1, acc1, true));
- assertEquals(Optional.of(URI.create("s3://bucket-1/")), bucketDb.archiveUriFor(z1, acc1, false));
-
- service.setEnclaveArchiveBucket(z1, acc1, "bucket-2");
- assertEquals(Optional.of(URI.create("s3://bucket-1/")), bucketDb.archiveUriFor(z1, acc1, true)); // Returns old value even with search
-
- clock.advance(Duration.ofMinutes(61)); // After expiry the cache is expired, new search is performed
- assertEquals(Optional.of(URI.create("s3://bucket-1/")), bucketDb.archiveUriFor(z1, acc1, false)); // When requesting without search, return previous value even if expired
- assertEquals(Optional.of(URI.create("s3://bucket-2/")), bucketDb.archiveUriFor(z1, acc1, true));
- assertEquals(Optional.of(URI.create("s3://bucket-2/")), bucketDb.archiveUriFor(z1, acc1, false));
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLoggerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLoggerTest.java
deleted file mode 100644
index 1920524823a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/auditlog/AuditLoggerTest.java
+++ /dev/null
@@ -1,109 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.auditlog;
-
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.jdisc.http.HttpRequest.Method;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLog.Entry;
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayInputStream;
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.function.Supplier;
-
-import static java.time.temporal.ChronoUnit.MILLIS;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-public class AuditLoggerTest {
-
- private final ControllerTester tester = new ControllerTester();
- private final Supplier<AuditLog> log = () -> tester.controller().auditLogger().readLog();
-
- @Test
- void test_logging() {
- { // GET request is ignored
- HttpRequest request = testRequest(Method.GET, URI.create("http://localhost:8080/os/v1/"), "");
- tester.controller().auditLogger().log(request);
- assertTrue(log.get().entries().isEmpty(), "Not logged");
- }
-
- { // PATCH request is logged in audit log
- URI url = URI.create("http://localhost:8080/os/v1/?foo=bar");
- String data = "{\"cloud\":\"cloud9\",\"version\":\"42.0\"}";
- HttpRequest request = testRequest(Method.PATCH, url, data);
- tester.controller().auditLogger().log(request);
- assertEntry(Entry.Method.PATCH, 1, "/os/v1/?foo=bar");
- assertEquals("user", log.get().entries().get(0).principal());
- assertEquals(data, log.get().entries().get(0).data().get());
- }
-
- { // Another PATCH request is logged
- tester.clock().advance(Duration.ofDays(1));
- HttpRequest request = testRequest(Method.PATCH, URI.create("http://localhost:8080/os/v1/"),
- "{\"cloud\":\"cloud9\",\"version\":\"43.0\"}");
- tester.controller().auditLogger().log(request);
- assertEntry(Entry.Method.PATCH, 2, "/os/v1/");
- }
-
- { // PUT is logged
- tester.clock().advance(Duration.ofDays(1));
- HttpRequest request = testRequest(Method.PUT, URI.create("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1/"),
- "");
- tester.controller().auditLogger().log(request);
- assertEntry(Entry.Method.PUT, 3, "/zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1/");
- }
-
- { // DELETE is logged
- tester.clock().advance(Duration.ofDays(1));
- HttpRequest request = testRequest(Method.DELETE, URI.create("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1"),
- "");
- tester.controller().auditLogger().log(request);
- assertEntry(Entry.Method.DELETE, 4, "/zone/v2/prod/us-north-1/nodes/v2/node/node1");
- }
-
- { // POST is logged
- tester.clock().advance(Duration.ofDays(1));
- HttpRequest request = testRequest(Method.POST, URI.create("http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.42"),
- "6.42");
- tester.controller().auditLogger().log(request);
- assertEntry(Entry.Method.POST, 5, "/controller/v1/jobs/upgrader/confidence/6.42");
- }
-
- { // 15 days pass and another PATCH request is logged. Older entries are removed due to expiry
- tester.clock().advance(Duration.ofDays(15));
- HttpRequest request = testRequest(Method.PATCH, URI.create("http://localhost:8080/os/v1/"),
- "{\"cloud\":\"cloud9\",\"version\":\"44.0\"}");
- tester.controller().auditLogger().log(request);
- assertEntry(Entry.Method.PATCH, 1, "/os/v1/");
- }
- }
-
- private Instant instant() {
- return tester.clock().instant().truncatedTo(MILLIS);
- }
-
- private void assertEntry(Entry.Method method, int logSize, String resource) {
- assertEquals(logSize, log.get().entries().size());
- assertEquals(instant(), log.get().entries().get(0).at());
- assertEquals(method, log.get().entries().get(0).method());
- assertEquals(resource, log.get().entries().get(0).resource());
- }
-
- private static HttpRequest testRequest(Method method, URI url, String data) {
- HttpRequest request = HttpRequest.createTestRequest(
- url.toString(),
- method,
- new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))
- );
- request.getJDiscRequest().setUserPrincipal(() -> "user");
- return request;
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java
deleted file mode 100644
index 378b92d37ce..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java
+++ /dev/null
@@ -1,486 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.certificate;
-
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.security.KeyAlgorithm;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.security.SignatureAlgorithm;
-import com.yahoo.security.X509CertificateBuilder;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.test.ManualClock;
-import com.yahoo.transaction.Mutex;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProviderMock;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidatorImpl;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidatorMock;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.integration.SecretStoreMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.routing.EndpointConfig;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import javax.security.auth.x500.X500Principal;
-import java.security.KeyPair;
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Stream;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author andreer
- */
-public class EndpointCertificatesTest {
-
- private final ControllerTester tester = new ControllerTester();
- private final SecretStoreMock secretStore = new SecretStoreMock();
- private final CuratorDb curator = tester.curator();
- private final ManualClock clock = tester.clock();
- private final EndpointCertificateProviderMock endpointCertificateProviderMock = new EndpointCertificateProviderMock();
- private final EndpointCertificateValidatorImpl endpointCertificateValidator = new EndpointCertificateValidatorImpl(secretStore, clock);
- private final EndpointCertificates endpointCertificates = new EndpointCertificates(tester.controller(), endpointCertificateProviderMock, endpointCertificateValidator);
- private final KeyPair testKeyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 192);
- private final Mutex lock = () -> {};
-
- private X509Certificate testCertificate;
- private X509Certificate testCertificate2;
-
- private static final List<String> expectedSans = List.of(
- "vt2ktgkqme5zlnp4tj4ttyor7fj3v7q5o.vespa.oath.cloud",
- "default.default.global.vespa.oath.cloud",
- "*.default.default.global.vespa.oath.cloud",
- "default.default.aws-us-east-1a.vespa.oath.cloud",
- "*.default.default.aws-us-east-1a.vespa.oath.cloud",
- "*.f5549014.z.vespa.oath.cloud",
- "*.f5549014.g.vespa.oath.cloud",
- "*.f5549014.a.vespa.oath.cloud",
- "default.default.us-east-1.test.vespa.oath.cloud",
- "*.default.default.us-east-1.test.vespa.oath.cloud",
- "default.default.us-east-3.staging.vespa.oath.cloud",
- "*.default.default.us-east-3.staging.vespa.oath.cloud"
- );
-
- private static final List<String> expectedAdditionalSans = List.of(
- "default.default.ap-northeast-1.vespa.oath.cloud",
- "*.default.default.ap-northeast-1.vespa.oath.cloud"
- );
-
- private static final List<String> expectedCombinedSans = new ArrayList<>() {{
- addAll(expectedSans);
- addAll(expectedAdditionalSans);
- }};
-
- private static final List<String> expectedDevSans = List.of(
- "vt2ktgkqme5zlnp4tj4ttyor7fj3v7q5o.vespa.oath.cloud",
- "default.default.us-east-1.dev.vespa.oath.cloud",
- "*.default.default.us-east-1.dev.vespa.oath.cloud",
- "*.f5549014.z.vespa.oath.cloud",
- "*.f5549014.g.vespa.oath.cloud",
- "*.f5549014.a.vespa.oath.cloud"
- );
-
- private X509Certificate makeTestCert(List<String> sans) {
- X509CertificateBuilder x509CertificateBuilder = X509CertificateBuilder
- .fromKeypair(
- testKeyPair,
- new X500Principal("CN=test"),
- clock.instant(), clock.instant().plus(5, ChronoUnit.MINUTES),
- SignatureAlgorithm.SHA256_WITH_ECDSA,
- X509CertificateBuilder.generateRandomSerialNumber());
- for (String san : sans) x509CertificateBuilder = x509CertificateBuilder.addSubjectAlternativeName(san);
- return x509CertificateBuilder.build();
- }
-
- private final ApplicationId instance = ApplicationId.defaultId();
- private final String testKeyName = "testKeyName";
- private final String testCertName = "testCertName";
- private ZoneId prodZone;
-
- @BeforeEach
- public void setUp() {
- tester.zoneRegistry().exclusiveRoutingIn(tester.zoneRegistry().zones().all().zones());
- prodZone = tester.zoneRegistry().zones().all().routingMethod(RoutingMethod.exclusive).in(Environment.prod).zones().stream().findFirst().orElseThrow().getId();
- clock.setInstant(Instant.EPOCH);
- testCertificate = makeTestCert(expectedSans);
- testCertificate2 = makeTestCert(expectedCombinedSans);
- }
-
- @Test
- void provisions_new_certificate_in_dev() {
- ZoneId testZone = tester.zoneRegistry().zones().all().routingMethod(RoutingMethod.exclusive).in(Environment.dev).zones().stream().findFirst().orElseThrow().getId();
- EndpointCertificate cert = endpointCertificates.get(new DeploymentId(instance, testZone), DeploymentSpec.empty, lock);
- assertTrue(cert.keyName().matches("vespa.tls.default.default.*-key"));
- assertTrue(cert.certName().matches("vespa.tls.default.default.*-cert"));
- assertEquals(0, cert.version());
- assertEquals(expectedDevSans, cert.requestedDnsSans());
- }
-
- @Test
- void provisions_new_certificate_in_prod() {
- EndpointCertificate cert = endpointCertificates.get(new DeploymentId(instance, prodZone), DeploymentSpec.empty, lock);
- assertTrue(cert.keyName().matches("vespa.tls.default.default.*-key"));
- assertTrue(cert.certName().matches("vespa.tls.default.default.*-cert"));
- assertEquals(0, cert.version());
- assertEquals(expectedSans, cert.requestedDnsSans());
- }
-
- private ControllerTester publicTester() {
- ControllerTester publicTester = new ControllerTester(SystemName.Public);
- publicTester.zoneRegistry().setZones(tester.zoneRegistry().zones().all().zones());
- return publicTester;
- }
-
- @Test
- void provisions_new_certificate_in_public_prod() {
- ControllerTester tester = publicTester();
- EndpointCertificateValidatorImpl endpointCertificateValidator = new EndpointCertificateValidatorImpl(secretStore, clock);
- EndpointCertificates endpointCertificates = new EndpointCertificates(tester.controller(), endpointCertificateProviderMock, endpointCertificateValidator);
- List<String> expectedSans = List.of(
- "vt2ktgkqme5zlnp4tj4ttyor7fj3v7q5o.internal.vespa-app.cloud",
- "default.default.g.vespa-app.cloud",
- "*.default.default.g.vespa-app.cloud",
- "default.default.aws-us-east-1a.z.vespa-app.cloud",
- "*.default.default.aws-us-east-1a.z.vespa-app.cloud",
- "*.f5549014.z.vespa-app.cloud",
- "*.f5549014.g.vespa-app.cloud",
- "*.f5549014.a.vespa-app.cloud",
- "default.default.us-east-1.test.z.vespa-app.cloud",
- "*.default.default.us-east-1.test.z.vespa-app.cloud",
- "default.default.us-east-3.staging.z.vespa-app.cloud",
- "*.default.default.us-east-3.staging.z.vespa-app.cloud"
- );
- EndpointCertificate cert = endpointCertificates.get(new DeploymentId(instance, prodZone), DeploymentSpec.empty, lock);
- assertTrue(cert.keyName().matches("vespa.tls.default.default.*-key"));
- assertTrue(cert.certName().matches("vespa.tls.default.default.*-cert"));
- assertEquals(0, cert.version());
- assertEquals(expectedSans, cert.requestedDnsSans());
- }
-
- @Test
- void reuses_stored_certificate() {
- curator.writeAssignedCertificate(assignedCertificate(instance, new EndpointCertificate(testKeyName, testCertName, 7, 0, "request_id", Optional.of("leaf-request-uuid"),
- List.of("vt2ktgkqme5zlnp4tj4ttyor7fj3v7q5o.vespa.oath.cloud",
- "default.default.global.vespa.oath.cloud",
- "*.default.default.global.vespa.oath.cloud",
- "default.default.aws-us-east-1a.vespa.oath.cloud",
- "*.default.default.aws-us-east-1a.vespa.oath.cloud",
- "*.f5549014.z.vespa.oath.cloud",
- "*.f5549014.g.vespa.oath.cloud",
- "*.f5549014.a.vespa.oath.cloud"),
- "", Optional.empty(), Optional.empty(), Optional.empty())));
- secretStore.setSecret(testKeyName, KeyUtils.toPem(testKeyPair.getPrivate()), 7);
- secretStore.setSecret(testCertName, X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), 7);
- EndpointCertificate cert = endpointCertificates.get(new DeploymentId(instance, prodZone), DeploymentSpec.empty, lock);
- assertEquals(testKeyName, cert.keyName());
- assertEquals(testCertName, cert.certName());
- assertEquals(7, cert.version());
- }
-
- @Test
- void reprovisions_certificate_when_necessary() {
- curator.writeAssignedCertificate(assignedCertificate(instance, new EndpointCertificate(testKeyName, testCertName, -1, 0, "root-request-uuid", Optional.of("leaf-request-uuid"), List.of(), "issuer", Optional.empty(), Optional.empty(), Optional.empty())));
- secretStore.setSecret("vespa.tls.default.default.default-key", KeyUtils.toPem(testKeyPair.getPrivate()), 0);
- secretStore.setSecret("vespa.tls.default.default.default-cert", X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), 0);
- EndpointCertificate cert = endpointCertificates.get(new DeploymentId(instance, prodZone), DeploymentSpec.empty, lock);
- assertEquals(0, cert.version());
- assertEquals(cert, curator.readAssignedCertificate(instance).map(AssignedCertificate::certificate).get());
- }
-
- @Test
- void reprovisions_certificate_with_added_sans_when_deploying_to_new_zone() {
- ZoneId testZone = ZoneId.from("prod.ap-northeast-1");
-
- curator.writeAssignedCertificate(assignedCertificate(instance, new EndpointCertificate(testKeyName, testCertName, -1, 0, "original-request-uuid", Optional.of("leaf-request-uuid"), expectedSans, "mockCa", Optional.empty(), Optional.empty(), Optional.empty())));
- secretStore.setSecret("vespa.tls.default.default.default-key", KeyUtils.toPem(testKeyPair.getPrivate()), -1);
- secretStore.setSecret("vespa.tls.default.default.default-cert", X509CertificateUtils.toPem(testCertificate) + X509CertificateUtils.toPem(testCertificate), -1);
-
- secretStore.setSecret("vespa.tls.default.default.default-key", KeyUtils.toPem(testKeyPair.getPrivate()), 0);
- secretStore.setSecret("vespa.tls.default.default.default-cert", X509CertificateUtils.toPem(testCertificate2) + X509CertificateUtils.toPem(testCertificate2), 0);
-
- EndpointCertificate cert = endpointCertificates.get(new DeploymentId(instance, testZone), DeploymentSpec.empty, lock);
- assertEquals(0, cert.version());
- assertEquals(cert, curator.readAssignedCertificate(instance).map(AssignedCertificate::certificate).get());
- assertEquals("original-request-uuid", cert.rootRequestId());
- assertNotEquals(Optional.of("leaf-request-uuid"), cert.leafRequestId());
- assertEquals(Set.copyOf(expectedCombinedSans), Set.copyOf(cert.requestedDnsSans()));
- }
-
- @Test
- void includes_zones_in_deployment_spec_when_deploying_to_staging() {
- DeploymentSpec deploymentSpec = new DeploymentSpecXmlReader(true).read(
- """
- <deployment version="1.0">
- <instance id="default">
- <prod>
- <region active="true">aws-us-east-1a</region>
- <region active="true">ap-northeast-1</region>
- </prod>
- </instance>
- </deployment>
- """
- );
-
- ZoneId testZone = tester.zoneRegistry().zones().all().in(Environment.staging).zones().stream().findFirst().orElseThrow().getId();
- EndpointCertificate cert = endpointCertificates.get(new DeploymentId(instance, testZone), deploymentSpec, lock);
- assertTrue(cert.keyName().matches("vespa.tls.default.default.*-key"));
- assertTrue(cert.certName().matches("vespa.tls.default.default.*-cert"));
- assertEquals(0, cert.version());
- assertEquals(Set.copyOf(expectedCombinedSans), Set.copyOf(cert.requestedDnsSans()));
- }
-
- @Test
- void includes_application_endpoint_when_declared() {
- ApplicationId instance = ApplicationId.from("t1", "a1", "default");
- ZoneId zone1 = ZoneId.from(Environment.prod, RegionName.from("aws-us-east-1c"));
- ZoneId zone2 = ZoneId.from(Environment.prod, RegionName.from("aws-us-west-2a"));
- ControllerTester tester = publicTester();
- tester.zoneRegistry().addZones(ZoneApiMock.newBuilder().with(CloudName.DEFAULT).with(zone1).build(),
- ZoneApiMock.newBuilder().with(CloudName.AWS).with(zone2).build());
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .instances("beta,main")
- .region(zone1.region())
- .region(zone2.region())
- .applicationEndpoint("a", "qrs", zone2.region().value(),
- Map.of(InstanceName.from("beta"), 2))
- .applicationEndpoint("b", "qrs", zone2.region().value(),
- Map.of(InstanceName.from("beta"), 1))
- .applicationEndpoint("c", "qrs",
- Map.of(zone1.region().value(), Map.of(InstanceName.from("beta"), 4,
- InstanceName.from("main"), 6),
- zone2.region().value(), Map.of(InstanceName.from("main"), 2)))
- .build();
- EndpointCertificateValidatorImpl endpointCertificateValidator = new EndpointCertificateValidatorImpl(secretStore, clock);
- EndpointCertificates endpointCertificates = new EndpointCertificates(tester.controller(), endpointCertificateProviderMock, endpointCertificateValidator);
- List<String> expectedSans = Stream.of(
- "vlfms2wpoa4nyrka2s5lktucypjtxkqhv.internal.vespa-app.cloud",
- "a1.t1.g.vespa-app.cloud",
- "*.a1.t1.g.vespa-app.cloud",
- "a1.t1.a.vespa-app.cloud",
- "*.a1.t1.a.vespa-app.cloud",
- "a1.t1.aws-us-east-1c.z.vespa-app.cloud",
- "*.a1.t1.aws-us-east-1c.z.vespa-app.cloud",
- "a1.t1.us-east-1.test.z.vespa-app.cloud",
- "*.a1.t1.us-east-1.test.z.vespa-app.cloud",
- "a1.t1.us-east-3.staging.z.vespa-app.cloud",
- "*.a1.t1.us-east-3.staging.z.vespa-app.cloud",
- "*.f5549014.z.vespa-app.cloud",
- "*.f5549014.g.vespa-app.cloud",
- "*.f5549014.a.vespa-app.cloud"
- ).sorted().toList();
- EndpointCertificate cert = endpointCertificates.get(new DeploymentId(instance, zone1), applicationPackage.deploymentSpec(), lock);
- assertTrue(cert.keyName().matches("vespa.tls.t1.a1.*-key"));
- assertTrue(cert.certName().matches("vespa.tls.t1.a1.*-cert"));
- assertEquals(0, cert.version());
- assertEquals(expectedSans, cert.requestedDnsSans().stream().sorted().toList());
- }
-
- @Test
- public void assign_certificate_from_pool() {
- setEndpointConfig(tester, EndpointConfig.generated);
- try {
- addCertificateToPool("bad0f00d", UnassignedCertificate.State.requested, tester);
- endpointCertificates.get(new DeploymentId(instance, prodZone), DeploymentSpec.empty, lock);
- fail("Expected exception as certificate is not ready");
- } catch (IllegalArgumentException ignored) {}
-
- // Advance clock to verify last requested time
- clock.advance(Duration.ofDays(3));
-
- // Certificate is assigned from pool instead. The previously assigned certificate will eventually be cleaned up
- // by EndpointCertificateMaintainer
- { // prod
- String certId = "bad0f00d";
- addCertificateToPool(certId, UnassignedCertificate.State.ready, tester);
- EndpointCertificate cert = endpointCertificates.get(new DeploymentId(instance, prodZone), DeploymentSpec.empty, lock);
- assertEquals(certId, cert.generatedId().get());
- assertEquals(certId, tester.curator().readAssignedCertificate(TenantAndApplicationId.from(instance), Optional.empty()).get().certificate().generatedId().get(), "Certificate is assigned at application-level");
- assertTrue(tester.controller().curator().readUnassignedCertificate(certId).isEmpty(), "Certificate is removed from pool");
- assertEquals(clock.instant().getEpochSecond(), cert.lastRequested());
- }
-
- { // dev
- String certId = "f00d0bad";
- addCertificateToPool(certId, UnassignedCertificate.State.ready, tester);
- ZoneId devZone = tester.zoneRegistry().zones().all().routingMethod(RoutingMethod.exclusive).in(Environment.dev).zones().stream().findFirst().orElseThrow().getId();
- EndpointCertificate cert = endpointCertificates.get(new DeploymentId(instance, devZone), DeploymentSpec.empty, lock);
- assertEquals(certId, cert.generatedId().get());
- assertEquals(certId, tester.curator().readAssignedCertificate(instance).get().certificate().generatedId().get(), "Certificate is assigned at instance-level");
- assertTrue(tester.controller().curator().readUnassignedCertificate(certId).isEmpty(), "Certificate is removed from pool");
- assertEquals(clock.instant().getEpochSecond(), cert.lastRequested());
- }
- }
-
- @Test
- public void certificate_migration() {
- // An application is initially deployed with legacy config (the default)
- ZoneId zone1 = ZoneId.from(Environment.prod, RegionName.from("aws-us-east-1c"));
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder().region(zone1.region())
- .build();
- ControllerTester tester = publicTester();
- EndpointCertificates endpointCertificates = new EndpointCertificates(tester.controller(), endpointCertificateProviderMock, new EndpointCertificateValidatorMock());
- ApplicationId instance = ApplicationId.from("t1", "a1", "default");
- DeploymentId deployment0 = new DeploymentId(instance, zone1);
- final EndpointCertificate certificate = endpointCertificates.get(deployment0, applicationPackage.deploymentSpec(), lock);
- final String generatedId = certificate.generatedId().get();
- assertEquals(List.of("vlfms2wpoa4nyrka2s5lktucypjtxkqhv.internal.vespa-app.cloud",
- "a1.t1.g.vespa-app.cloud",
- "*.a1.t1.g.vespa-app.cloud",
- "a1.t1.aws-us-east-1c.z.vespa-app.cloud",
- "*.a1.t1.aws-us-east-1c.z.vespa-app.cloud",
- "*.f5549014.z.vespa-app.cloud",
- "*.f5549014.g.vespa-app.cloud",
- "*.f5549014.a.vespa-app.cloud",
- "a1.t1.us-east-1.test.z.vespa-app.cloud",
- "*.a1.t1.us-east-1.test.z.vespa-app.cloud",
- "a1.t1.us-east-3.staging.z.vespa-app.cloud",
- "*.a1.t1.us-east-3.staging.z.vespa-app.cloud"),
- certificate.requestedDnsSans());
- Optional<AssignedCertificate> assignedCertificate = tester.curator().readAssignedCertificate(deployment0.applicationId());
- assertTrue(assignedCertificate.isPresent(), "Certificate is assigned at instance level");
- assertTrue(assignedCertificate.get().certificate().generatedId().isPresent(), "Certificate contains generated ID");
-
- // Re-requesting certificate does not make any changes, except last requested time
- tester.clock().advance(Duration.ofHours(1));
- assertEquals(certificate.withLastRequested(tester.clock().instant().getEpochSecond()),
- endpointCertificates.get(deployment0, applicationPackage.deploymentSpec(), lock),
- "Next request returns same certificate and updates last requested time");
-
- // An additional instance is added to deployment spec
- applicationPackage = new ApplicationPackageBuilder().instances("default,beta")
- .region(zone1.region())
- .build();
- DeploymentId deployment1 = new DeploymentId(ApplicationId.from("t1", "a1", "beta"), zone1);
- EndpointCertificate betaCert = endpointCertificates.get(deployment1, applicationPackage.deploymentSpec(), lock);
- assertEquals(List.of("v43ctkgqim52zsbwefrg6ixkuwidvsumy.internal.vespa-app.cloud",
- "beta.a1.t1.g.vespa-app.cloud",
- "*.beta.a1.t1.g.vespa-app.cloud",
- "beta.a1.t1.aws-us-east-1c.z.vespa-app.cloud",
- "*.beta.a1.t1.aws-us-east-1c.z.vespa-app.cloud",
- "*.f5549014.z.vespa-app.cloud",
- "*.f5549014.g.vespa-app.cloud",
- "*.f5549014.a.vespa-app.cloud",
- "beta.a1.t1.us-east-1.test.z.vespa-app.cloud",
- "*.beta.a1.t1.us-east-1.test.z.vespa-app.cloud",
- "beta.a1.t1.us-east-3.staging.z.vespa-app.cloud",
- "*.beta.a1.t1.us-east-3.staging.z.vespa-app.cloud"),
- betaCert.requestedDnsSans());
- assertEquals(generatedId, betaCert.generatedId().get(), "Certificate inherits generated ID of existing instance");
-
- // A dev instance is deployed
- DeploymentId devDeployment0 = new DeploymentId(ApplicationId.from("t1", "a1", "dev"),
- ZoneId.from("dev", "us-east-1"));
- EndpointCertificate devCert0 = endpointCertificates.get(devDeployment0, applicationPackage.deploymentSpec(), lock);
- assertNotEquals(generatedId, devCert0.generatedId().get(), "Dev deployments gets a new generated ID");
- assertEquals(List.of("vld3y4mggzpd5wmm5jmldzcbyetjoqtzq.internal.vespa-app.cloud",
- "dev.a1.t1.us-east-1.dev.z.vespa-app.cloud",
- "*.dev.a1.t1.us-east-1.dev.z.vespa-app.cloud",
- "*.a89ff7c6.z.vespa-app.cloud",
- "*.a89ff7c6.g.vespa-app.cloud",
- "*.a89ff7c6.a.vespa-app.cloud"),
- devCert0.requestedDnsSans());
-
- // Application switches to combined config
- setEndpointConfig(tester, EndpointConfig.combined);
- tester.clock().advance(Duration.ofHours(1));
- assertEquals(certificate.withLastRequested(tester.clock().instant().getEpochSecond()),
- endpointCertificates.get(deployment0, applicationPackage.deploymentSpec(), lock),
- "No change to certificate: Existing certificate is compatible with " +
- EndpointConfig.combined + " config");
- assertTrue(tester.curator().readAssignedCertificate(deployment0.applicationId()).isPresent(), "Certificate is assigned at instance level");
- assertFalse(tester.curator().readAssignedCertificate(TenantAndApplicationId.from(deployment0.applicationId()), Optional.empty()).isPresent(),
- "Certificate is not assigned at application level");
-
- // Application switches to generated config
- setEndpointConfig(tester, EndpointConfig.generated);
- tester.clock().advance(Duration.ofHours(1));
- assertEquals(certificate.withLastRequested(tester.clock().instant().getEpochSecond()),
- endpointCertificates.get(deployment0, applicationPackage.deploymentSpec(), lock),
- "No change to certificate: Existing certificate is compatible with " +
- EndpointConfig.generated + " config");
- assertFalse(tester.curator().readAssignedCertificate(deployment0.applicationId()).isPresent(), "Certificate is no longer assigned at instance level");
- assertTrue(tester.curator().readAssignedCertificate(TenantAndApplicationId.from(deployment0.applicationId()), Optional.empty()).isPresent(),
- "Certificate is assigned at application level");
-
- // Both instances still use the same certificate
- assertEquals(endpointCertificates.get(deployment0, applicationPackage.deploymentSpec(), lock),
- endpointCertificates.get(deployment1, applicationPackage.deploymentSpec(), lock));
-
- // Another dev instance is deployed, and is assigned certificate from pool
- String poolCertId0 = "badf00d0";
- addCertificateToPool(poolCertId0, UnassignedCertificate.State.ready, tester);
- EndpointCertificate devCert1 = endpointCertificates.get(new DeploymentId(ApplicationId.from("t1", "a1", "dev2"),
- ZoneId.from("dev", "us-east-1")),
- applicationPackage.deploymentSpec(), lock);
- assertEquals(poolCertId0, devCert1.generatedId().get());
-
- // Another application is deployed, and is assigned certificate from pool
- String poolCertId1 = "badf00d1";
- addCertificateToPool(poolCertId1, UnassignedCertificate.State.ready, tester);
- EndpointCertificate prodCertificate = endpointCertificates.get(new DeploymentId(ApplicationId.from("t1", "a2", "default"),
- ZoneId.from("prod", "us-east-1")),
- applicationPackage.deploymentSpec(), lock);
- assertEquals(poolCertId1, prodCertificate.generatedId().get());
-
- // Application switches back to legacy config
- setEndpointConfig(tester, EndpointConfig.legacy);
- EndpointCertificate reissuedCertificate = endpointCertificates.get(deployment0, applicationPackage.deploymentSpec(), lock);
- assertEquals(certificate.requestedDnsSans(), reissuedCertificate.requestedDnsSans());
- assertTrue(tester.curator().readAssignedCertificate(deployment0.applicationId()).isPresent(), "Certificate is assigned at instance level again");
- assertTrue(tester.curator().readAssignedCertificate(TenantAndApplicationId.from(deployment0.applicationId()), Optional.empty()).isPresent(),
- "Certificate is still assigned at application level"); // Not removed because the assumption is that the application will eventually migrate back
- }
-
- private void setEndpointConfig(ControllerTester tester, EndpointConfig config) {
- tester.flagSource().withStringFlag(Flags.ENDPOINT_CONFIG.id(), config.name());
- }
-
- private void addCertificateToPool(String id, UnassignedCertificate.State state, ControllerTester tester) {
- EndpointCertificate cert = new EndpointCertificate(testKeyName,
- testCertName,
- 1,
- 0,
- "request-id",
- Optional.of("leaf-request-uuid"),
- List.of("*." + id + ".z.vespa.oath.cloud",
- "*." + id + ".g.vespa.oath.cloud",
- "*." + id + ".a.vespa.oath.cloud"),
- "",
- Optional.empty(),
- Optional.empty(),
- Optional.of(id));
- UnassignedCertificate pooledCert = new UnassignedCertificate(cert, state);
- tester.controller().curator().writeUnassignedCertificate(pooledCert);
- }
-
- private static AssignedCertificate assignedCertificate(ApplicationId instance, EndpointCertificate certificate) {
- return new AssignedCertificate(TenantAndApplicationId.from(instance), Optional.of(instance.instance()), certificate, false);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/concurrent/OnceTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/concurrent/OnceTest.java
deleted file mode 100644
index 8c2815c8646..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/concurrent/OnceTest.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.concurrent;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
-
-import java.time.Duration;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-public class OnceTest {
-
- @Test
- @Timeout(60_000)
- void test_run() throws Exception {
- CountDownLatch latch = new CountDownLatch(1);
- Once.after(Duration.ZERO, latch::countDown);
-
- assertTrue(latch.await(30, TimeUnit.SECONDS));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java
deleted file mode 100644
index de915229cd4..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ApplicationPackageBuilder.java
+++ /dev/null
@@ -1,444 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.AthenzService;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.security.SignatureAlgorithm;
-import com.yahoo.security.X509CertificateBuilder;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-
-import javax.security.auth.x500.X500Principal;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.security.KeyPairGenerator;
-import java.security.NoSuchAlgorithmException;
-import java.security.cert.X509Certificate;
-import java.text.SimpleDateFormat;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.OptionalInt;
-import java.util.StringJoiner;
-import java.util.TreeMap;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import java.util.zip.Deflater;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * A builder that builds application packages for testing purposes.
- *
- * @author mpolden
- */
-public class ApplicationPackageBuilder {
-
- private final StringBuilder prodBody = new StringBuilder();
- private final StringBuilder validationOverridesBody = new StringBuilder();
- private final StringBuilder blockChange = new StringBuilder();
- private final StringJoiner notifications = new StringJoiner("/>\n <email ",
- "<notifications>\n <email ",
- "/>\n</notifications>\n").setEmptyValue("");
- private final StringBuilder endpointsBody = new StringBuilder();
- private final StringBuilder applicationEndpointsBody = new StringBuilder();
- private final StringBuilder servicesBody = new StringBuilder();
- private final List<X509Certificate> trustedCertificates = new ArrayList<>();
- private final Map<Environment, Map<String, String>> nonProductionEnvironments = new LinkedHashMap<>();
-
- private OptionalInt majorVersion = OptionalInt.empty();
- private String instances = "default";
- private String upgradePolicy = null;
- private String revisionTarget = "latest";
- private String revisionChange = "always";
- private String upgradeRollout = null;
- private String athenzIdentityAttributes = "athenz-domain='domain' athenz-service='service'";
- private String searchDefinition = "search test { }";
- private Version compileVersion = Version.fromString("6.1");
- private String cloudAccount = null;
-
- public ApplicationPackageBuilder majorVersion(int majorVersion) {
- this.majorVersion = OptionalInt.of(majorVersion);
- return this;
- }
-
- public ApplicationPackageBuilder instances(String instances) {
- this.instances = instances;
- return this;
- }
-
- public ApplicationPackageBuilder upgradePolicy(String upgradePolicy) {
- this.upgradePolicy = upgradePolicy;
- return this;
- }
-
- public ApplicationPackageBuilder revisionTarget(String revisionTarget) {
- this.revisionTarget = revisionTarget;
- return this;
- }
-
- public ApplicationPackageBuilder revisionChange(String revisionChange) {
- this.revisionChange = revisionChange;
- return this;
- }
-
- public ApplicationPackageBuilder upgradeRollout(String upgradeRollout) {
- this.upgradeRollout = upgradeRollout;
- return this;
- }
-
- public ApplicationPackageBuilder endpoint(String id, String containerId, String... regions) {
- endpointsBody.append(" <endpoint");
- endpointsBody.append(" id='").append(id).append("'");
- endpointsBody.append(" container-id='").append(containerId).append("'");
- endpointsBody.append(">\n");
- for (var region : regions) {
- endpointsBody.append(" <region>").append(region).append("</region>\n");
- }
- endpointsBody.append(" </endpoint>\n");
- return this;
- }
-
- public ApplicationPackageBuilder container(String id, AuthMethod... authMethod) {
- servicesBody.append(" <container id='")
- .append(id)
- .append("'>\n")
- .append(" <clients>\n");
- for (int i = 0; i < authMethod.length; i++) {
- AuthMethod m = authMethod[i];
- servicesBody.append(" <client id='")
- .append("client-").append(m.name()).append("-").append(i)
- .append("'>\n");
- if (m == AuthMethod.token) {
- servicesBody.append(" <token id='")
- .append(m.name()).append("-").append(i)
- .append("'/>\n");
- }
- servicesBody.append(" </client>\n");
- }
- servicesBody.append(" </clients>\n")
- .append(" </container>\n");
- return this;
- }
-
- public ApplicationPackageBuilder applicationEndpoint(String id, String containerId, String region,
- Map<InstanceName, Integer> instanceWeights) {
- return applicationEndpoint(id, containerId, Map.of(region, instanceWeights));
- }
-
- public ApplicationPackageBuilder applicationEndpoint(String id, String containerId,
- Map<String, Map<InstanceName, Integer>> instanceWeights) {
- if (instanceWeights.isEmpty()) throw new IllegalArgumentException("At least one instance must be given");
- applicationEndpointsBody.append(" <endpoint");
- applicationEndpointsBody.append(" id='").append(id).append("'");
- applicationEndpointsBody.append(" container-id='").append(containerId).append("'");
- applicationEndpointsBody.append(">\n");
- new TreeMap<>(instanceWeights).forEach((region, instances) -> {
- new TreeMap<>(instances).forEach((instance, weight) -> {
- applicationEndpointsBody.append(" <instance weight='").append(weight.toString()).append("' region='").append(region).append("'>")
- .append(instance)
- .append("</instance>\n");
-
- });
- });
- applicationEndpointsBody.append(" </endpoint>\n");
- return this;
- }
-
- public ApplicationPackageBuilder systemTest() {
- return explicitEnvironment(Environment.test);
- }
-
- public ApplicationPackageBuilder stagingTest() {
- return explicitEnvironment(Environment.staging);
- }
-
- public ApplicationPackageBuilder explicitEnvironment(Environment environment, Environment... rest) {
- Stream.concat(Stream.of(environment), Arrays.stream(rest))
- .forEach(env -> nonProductionEnvironment(env, Map.of()));
- return this;
- }
-
- private ApplicationPackageBuilder nonProductionEnvironment(Environment environment, Map<String, String> attributes) {
- if (environment.isProduction()) throw new IllegalArgumentException("Expected non-production environment, got " + environment);
- nonProductionEnvironments.put(environment, attributes);
- return this;
- }
-
- public ApplicationPackageBuilder region(String regionName) {
- return region(RegionName.from(regionName));
- }
-
- public ApplicationPackageBuilder region(String regionName, String cloudAccount) {
- return region(RegionName.from(regionName), cloudAccount);
- }
-
- public ApplicationPackageBuilder region(RegionName regionName, String cloudAccount) {
- prodBody.append(" <region ")
- .append("cloud-account=\"")
- .append(cloudAccount)
- .append("\">")
- .append(regionName)
- .append("</region>\n");
- return this;
- }
-
- public ApplicationPackageBuilder region(RegionName regionName) {
- prodBody.append(" <region>")
- .append(regionName.value())
- .append("</region>\n");
- return this;
- }
-
- public ApplicationPackageBuilder test(String regionName) {
- prodBody.append(" <test>");
- prodBody.append(regionName);
- prodBody.append("</test>\n");
- return this;
- }
-
- public ApplicationPackageBuilder parallel(String... regionName) {
- prodBody.append(" <parallel>\n");
- Arrays.stream(regionName).forEach(this::region);
- prodBody.append(" </parallel>\n");
- return this;
- }
-
- public ApplicationPackageBuilder delay(Duration delay) {
- prodBody.append(" <delay seconds='");
- prodBody.append(delay.getSeconds());
- prodBody.append("'/>\n");
- return this;
- }
-
- public ApplicationPackageBuilder blockChange(boolean revision, boolean version, String daySpec, String hourSpec,
- String zoneSpec) {
- blockChange.append(" <block-change");
- blockChange.append(" revision='").append(revision).append("'");
- blockChange.append(" version='").append(version).append("'");
- blockChange.append(" days='").append(daySpec).append("'");
- blockChange.append(" hours='").append(hourSpec).append("'");
- blockChange.append(" time-zone='").append(zoneSpec).append("'");
- blockChange.append("/>\n");
- return this;
- }
-
- public ApplicationPackageBuilder allow(ValidationId validationId) {
- validationOverridesBody.append(" <allow until='");
- validationOverridesBody.append(asIso8601Date(Instant.now().plus(Duration.ofDays(28))));
- validationOverridesBody.append("'>");
- validationOverridesBody.append(validationId.value());
- validationOverridesBody.append("</allow>\n");
- return this;
- }
-
- public ApplicationPackageBuilder compileVersion(Version version) {
- compileVersion = version;
- return this;
- }
-
- public ApplicationPackageBuilder athenzIdentity(AthenzDomain domain, AthenzService service) {
- this.athenzIdentityAttributes = Text.format("athenz-domain='%s' athenz-service='%s'", domain.value(),
- service.value());
- return this;
- }
-
- public ApplicationPackageBuilder withoutAthenzIdentity() {
- this.athenzIdentityAttributes = null;
- return this;
- }
-
- public ApplicationPackageBuilder emailRole(String role) {
- this.notifications.add("role=\"" + role + "\"");
- return this;
- }
-
- public ApplicationPackageBuilder emailAddress(String address) {
- this.notifications.add("address=\"" + address + "\"");
- return this;
- }
-
- /** Sets the content of the search definition test.sd */
- public ApplicationPackageBuilder searchDefinition(String testSearchDefinition) {
- this.searchDefinition = testSearchDefinition;
- return this;
- }
-
- /** Add a trusted certificate to security/clients.pem */
- public ApplicationPackageBuilder trust(X509Certificate certificate) {
- this.trustedCertificates.add(certificate);
- return this;
- }
-
- /** Add a default trusted certificate to security/clients.pem */
- public ApplicationPackageBuilder trustDefaultCertificate() {
- try {
- var generator = KeyPairGenerator.getInstance("RSA");
- var certificate = X509CertificateBuilder.fromKeypair(
- generator.generateKeyPair(),
- new X500Principal("CN=name"),
- Instant.now(),
- Instant.now().plusMillis(300_000),
- SignatureAlgorithm.SHA256_WITH_RSA,
- X509CertificateBuilder.generateRandomSerialNumber()
- ).build();
- return trust(certificate);
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException(e);
- }
- }
-
- public ApplicationPackageBuilder cloudAccount(String cloudAccount) {
- this.cloudAccount = cloudAccount;
- return this;
- }
-
- public ApplicationPackageBuilder cloudAccount(Environment environment, String cloudAccount) {
- return nonProductionEnvironment(environment, Map.of("cloud-account", cloudAccount));
- }
-
- private byte[] deploymentSpec() {
- StringBuilder xml = new StringBuilder();
- xml.append("<deployment version='1.0' ");
- majorVersion.ifPresent(v -> xml.append("major-version='").append(v).append("' "));
- if (athenzIdentityAttributes != null) {
- xml.append(athenzIdentityAttributes);
- }
- if (cloudAccount != null) {
- xml.append(" cloud-account='");
- xml.append(cloudAccount);
- xml.append("'");
- }
- xml.append(">\n");
- for (String instance : instances.split(",")) {
- xml.append(" <instance id='").append(instance).append("'>\n");
- if (upgradePolicy != null || revisionTarget != null || revisionChange != null || upgradeRollout != null) {
- xml.append(" <upgrade ");
- if (upgradePolicy != null) xml.append("policy='").append(upgradePolicy).append("' ");
- if (revisionTarget != null) xml.append("revision-target='").append(revisionTarget).append("' ");
- if (revisionChange != null) xml.append("revision-change='").append(revisionChange).append("' ");
- if (upgradeRollout != null) xml.append("rollout='").append(upgradeRollout).append("' ");
- xml.append("/>\n");
- }
- xml.append(notifications);
- nonProductionEnvironments.forEach((environment, attributes) -> {
- xml.append(" <").append(environment.value());
- attributes.forEach((attribute, value) -> {
- xml.append(" ").append(attribute).append("='").append(value).append("'");
- });
- xml.append(" />\n");
- });
- xml.append(blockChange);
- xml.append(" <prod>\n");
- xml.append(prodBody);
- xml.append(" </prod>\n");
- if (endpointsBody.length() > 0) {
- xml.append(" <endpoints>\n");
- xml.append(endpointsBody);
- xml.append(" </endpoints>\n");
- }
- xml.append(" </instance>\n");
- }
- if (applicationEndpointsBody.length() > 0) {
- xml.append(" <endpoints>\n");
- xml.append(applicationEndpointsBody);
- xml.append(" </endpoints>\n");
- }
- xml.append("</deployment>\n");
- return xml.toString().getBytes(UTF_8);
- }
-
- private byte[] validationOverrides() {
- String xml = "<validation-overrides version='1.0'>\n" +
- validationOverridesBody +
- "</validation-overrides>\n";
- return xml.getBytes(UTF_8);
- }
-
- private byte[] searchDefinition() {
- return searchDefinition.getBytes(UTF_8);
- }
-
- private byte[] services() {
- return ("<services version='1.0'>\n" + servicesBody + "</services>\n").getBytes(UTF_8);
- }
-
- private static byte[] buildMeta(Version compileVersion) {
- return compileVersion == null ? new byte[0]
- : ("{\"compileVersion\":\"" + compileVersion.toFullString() +
- "\",\"buildTime\":1000,\"parentVersion\":\"" +
- compileVersion.toFullString() + "\"}").getBytes(UTF_8);
- }
-
- public ApplicationPackage build() {
- ByteArrayOutputStream zip = new ByteArrayOutputStream();
- try (ZipOutputStream out = new ZipOutputStream(zip)) {
- out.setLevel(Deflater.NO_COMPRESSION); // This is for testing purposes so we skip compression for performance
- writeZipEntry(out, "deployment.xml", deploymentSpec());
- writeZipEntry(out, "services.xml", services());
- writeZipEntry(out, "validation-overrides.xml", validationOverrides());
- writeZipEntry(out, "schemas/test.sd", searchDefinition());
- writeZipEntry(out, "build-meta.json", buildMeta(compileVersion));
- if (!trustedCertificates.isEmpty()) {
- writeZipEntry(out, "security/clients.pem", X509CertificateUtils.toPem(trustedCertificates).getBytes(UTF_8));
- }
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- return new ApplicationPackage(zip.toByteArray());
- }
-
- private void writeZipEntry(ZipOutputStream out, String name, byte[] content) throws IOException {
- ZipEntry entry = new ZipEntry(name);
- out.putNextEntry(entry);
- out.write(content);
- out.closeEntry();
- }
-
- private static String asIso8601Date(Instant instant) {
- return new SimpleDateFormat("yyyy-MM-dd").format(Date.from(instant));
- }
-
- public static ApplicationPackage fromDeploymentXml(String deploymentXml, ValidationId... overrides) {
- return fromDeploymentXml(deploymentXml, "6.1", overrides);
- }
-
- public static ApplicationPackage fromDeploymentXml(String deploymentXml, String compileVersion, ValidationId... overrides) {
- ByteArrayOutputStream zip = new ByteArrayOutputStream();
- try (ZipOutputStream out = new ZipOutputStream(zip)) {
- out.putNextEntry(new ZipEntry("deployment.xml"));
- out.write(deploymentXml.getBytes(UTF_8));
- out.closeEntry();
- out.putNextEntry(new ZipEntry("build-meta.json"));
- out.write(buildMeta(Version.fromString(compileVersion)));
- out.closeEntry();
- if (overrides.length > 0) {
- out.putNextEntry(new ZipEntry("validation-overrides.xml"));
- String override = "<allow until='" + asIso8601Date(Instant.now().plus(Duration.ofDays(28))) + "'>%s</allow>";
- out.write(("<validation-overrides version='1.0'>\n" +
- Arrays.stream(overrides).map(ValidationId::value).map(override::formatted).collect(Collectors.joining("\n")) +
- "</validation-overrides>\n").getBytes(UTF_8));
- out.closeEntry();
- }
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- return new ApplicationPackage(zip.toByteArray());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
deleted file mode 100644
index 841c54feb05..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentContext.java
+++ /dev/null
@@ -1,669 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.google.common.base.Suppliers;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.AthenzService;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.security.KeyAlgorithm;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.security.SignatureAlgorithm;
-import com.yahoo.security.X509CertificateBuilder;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException.ErrorCode;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Status;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.InternalStepRunner.Timeouts;
-import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock;
-import com.yahoo.vespa.hosted.controller.maintenance.JobRunner;
-import com.yahoo.vespa.hosted.controller.maintenance.NameServiceDispatcher;
-
-import javax.security.auth.x500.X500Principal;
-import java.math.BigInteger;
-import java.security.KeyPair;
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.function.Supplier;
-
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.failed;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotSame;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * A deployment context for an application. This allows fine-grained control of the deployment of an application's
- * instances.
- *
- * References to this should be acquired through {@link DeploymentTester#newDeploymentContext}.
- *
- * Tester code that is not specific to a single application's deployment context should be added to either
- * {@link ControllerTester} or {@link DeploymentTester} instead of this class.
- *
- * @author mpolden
- * @author jonmv
- */
-public class DeploymentContext {
-
- public static final JobType systemTest = JobType.deploymentTo(ZoneId.from("test", "us-east-1"));
- public static final JobType stagingTest = JobType.deploymentTo(ZoneId.from("staging", "us-east-3"));
- public static final JobType productionUsEast3 = JobType.prod("us-east-3");
- public static final JobType testUsEast3 = JobType.test("us-east-3");
- public static final JobType productionUsWest1 = JobType.prod("us-west-1");
- public static final JobType testUsWest1 = JobType.test("us-west-1");
- public static final JobType productionUsCentral1 = JobType.prod("us-central-1");
- public static final JobType testUsCentral1 = JobType.test("us-central-1");
- public static final JobType productionApNortheast1 = JobType.prod("ap-northeast-1");
- public static final JobType testApNortheast1 = JobType.test("ap-northeast-1");
- public static final JobType productionApNortheast2 = JobType.prod("ap-northeast-2");
- public static final JobType testApNortheast2 = JobType.test("ap-northeast-2");
- public static final JobType productionApSoutheast1 = JobType.prod("ap-southeast-1");
- public static final JobType testApSoutheast1 = JobType.test("ap-southeast-1");
- public static final JobType productionEuWest1 = JobType.prod("eu-west-1");
- public static final JobType testEuWest1 = JobType.test("eu-west-1");
- public static final JobType productionAwsUsEast1a = JobType.prod("aws-us-east-1a");
- public static final JobType testAwsUsEast1a = JobType.test("aws-us-east-1a");
- public static final JobType devUsEast1 = JobType.dev("us-east-1");
- public static final JobType devAwsUsEast2a = JobType.dev("aws-us-east-2a");
- public static final JobType perfUsEast3 = JobType.perf("us-east-3");
-
- private final AtomicLong salt = new AtomicLong();
-
- // Application packages are expensive to construct, and a given test typically only needs to the test in the context
- // of a single system. Construct them lazily.
- private static final Supplier<ApplicationPackage> applicationPackage = Suppliers.memoize(() -> new ApplicationPackageBuilder()
- .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"))
- .upgradePolicy("default")
- .region("us-central-1")
- .parallel("us-west-1", "us-east-3")
- .emailRole("author")
- .emailAddress("b@a")
- .build())::get;
-
- private static final Supplier<ApplicationPackage> publicCdApplicationPackage = Suppliers.memoize(() -> new ApplicationPackageBuilder()
- .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"))
- .upgradePolicy("default")
- .region("aws-us-east-1c")
- .emailRole("author")
- .emailAddress("b@a")
- .trust(generateCertificate())
- .build())::get;
-
- public static final SourceRevision defaultSourceRevision = new SourceRevision("repository1", "master", "commit1");
-
- private final TenantAndApplicationId applicationId;
- private final ApplicationId instanceId;
- private final TesterId testerId;
- private final JobController jobs;
- private final JobRunner runner;
- private final DeploymentTester tester;
-
- private RevisionId lastSubmission = null;
- private boolean deferDnsUpdates = false;
-
- public DeploymentContext(ApplicationId instanceId, DeploymentTester tester) {
- this.applicationId = TenantAndApplicationId.from(instanceId);
- this.instanceId = instanceId;
- this.testerId = TesterId.of(instanceId);
- this.jobs = tester.controller().jobController();
- this.runner = tester.runner();
- this.tester = tester;
- createTenantAndApplication();
- }
-
- public static ApplicationPackage applicationPackage() {
- return applicationPackage.get();
- }
-
- public static ApplicationPackage publicApplicationPackage() {
- return publicCdApplicationPackage.get();
- }
-
- private void createTenantAndApplication() {
- try {
- var tenant = tester.controllerTester().createTenant(instanceId.tenant().value());
- tester.controllerTester().createApplication(tenant.value(), instanceId.application().value(), instanceId.instance().value());
- } catch (IllegalArgumentException ignored) { } // Tenant and or application may already exist with custom setup.
- }
-
- public Application application() {
- return tester.controller().applications().requireApplication(applicationId);
- }
-
- public Instance instance() {
- return tester.controller().applications().requireInstance(instanceId);
- }
-
- public DeploymentStatus deploymentStatus() {
- return tester.controller().jobController().deploymentStatus(application());
- }
-
- public Map<JobType, JobStatus> instanceJobs() {
- return deploymentStatus().instanceJobs(instanceId.instance());
- }
-
- public Deployment deployment(ZoneId zone) {
- return instance().deployments().get(zone);
- }
-
- public ApplicationId instanceId() {
- return instanceId;
- }
-
- public TesterId testerId() { return testerId; }
-
- public DeploymentId deploymentIdIn(ZoneId zone) {
- return new DeploymentId(instanceId, zone);
- }
-
-
- /** Completely deploy the current change */
- public DeploymentContext deploy() {
- Application application = application();
- assertTrue(application.revisions().last().isPresent(), "Application package submitted");
- assertFalse(application.instances().values().stream()
- .anyMatch(instance -> instance.deployments().values().stream()
- .anyMatch(deployment -> deployment.revision().equals(lastSubmission))),
- "Submission is not already deployed");
- completeRollout(application.deploymentSpec().instances().size() > 1);
- for (var instance : application().instances().values()) {
- assertFalse(instance.change().hasTargets());
- }
- return this;
- }
-
- /** Upgrade platform of this to given version */
- public DeploymentContext deployPlatform(Version version) {
- assertEquals(instance().change().platform().get(), version);
- assertFalse(application().instances().values().stream()
- .anyMatch(instance -> instance.deployments().values().stream()
- .anyMatch(deployment -> deployment.version().equals(version))));
- assertEquals(version, instance().change().platform().get());
- assertFalse(instance().change().revision().isPresent());
-
- completeRollout();
-
- assertTrue(application().productionDeployments().values().stream()
- .allMatch(deployments -> deployments.stream()
- .allMatch(deployment -> deployment.version().equals(version))));
-
- for (JobId job : deploymentStatus().jobs().matching(job -> job.id().type().isProduction()).mapToList(JobStatus::id))
- assertTrue(tester.configServer().nodeRepository()
- .list(job.type().zone(),
- NodeFilter.all().applications(job.application())).stream()
- .allMatch(node -> node.currentVersion().equals(version)));
-
- assertFalse(instance().change().hasTargets());
- return this;
- }
-
- /** Defer provisioning of load balancers in zones in given environment */
- public DeploymentContext deferLoadBalancerProvisioningIn(Environment... environment) {
- return deferLoadBalancerProvisioningIn(Set.of(environment));
- }
-
- public DeploymentContext deferLoadBalancerProvisioningIn(Set<Environment> environments) {
- configServer().deferLoadBalancerProvisioningIn(environments);
- return this;
- }
-
- /** Defer DNS updates */
- public DeploymentContext deferDnsUpdates() {
- deferDnsUpdates = true;
- return this;
- }
-
- /** Flush all pending DNS updates */
- public DeploymentContext flushDnsUpdates() {
- flushDnsUpdates(Integer.MAX_VALUE);
- assertEquals(List.of(),
- tester.controller().curator().readNameServiceQueue().requests(),
- "All name service requests dispatched");
- return this;
- }
-
- /** Flush count pending DNS updates */
- public DeploymentContext flushDnsUpdates(int count) {
- var dispatcher = new NameServiceDispatcher(tester.controller(), Duration.ofSeconds(count));
- try {
- dispatcher.run();
- return this;
- } finally {
- dispatcher.awaitShutdown();
- }
- }
-
- /** Submit given application package for deployment */
- public DeploymentContext resubmit(ApplicationPackage applicationPackage) {
- return submit(applicationPackage, Optional.of(defaultSourceRevision), salt.get(), 0);
- }
-
- /** Submit given application package for deployment */
- public DeploymentContext submit(ApplicationPackage applicationPackage, int risk) {
- return submit(applicationPackage, Optional.of(defaultSourceRevision), salt.incrementAndGet(), risk);
- }
-
- /** Submit given application package for deployment */
- public DeploymentContext submit(ApplicationPackage applicationPackage) {
- return submit(applicationPackage, Optional.of(defaultSourceRevision));
- }
-
- /** Submit given application package for deployment */
- public DeploymentContext submit(ApplicationPackage applicationPackage, long salt, int risk) {
- return submit(applicationPackage, Optional.of(defaultSourceRevision), salt, risk);
- }
-
- /** Submit given application package for deployment */
- public DeploymentContext submit(ApplicationPackage applicationPackage, Optional<SourceRevision> sourceRevision) {
- return submit(applicationPackage, sourceRevision, salt.incrementAndGet(), 0);
- }
-
- /** Submit given application package for deployment */
- public DeploymentContext submit(ApplicationPackage applicationPackage, Optional<SourceRevision> sourceRevision, long salt, int risk) {
- var projectId = tester.controller().applications()
- .requireApplication(applicationId)
- .projectId()
- .orElse(1000); // These are really set through submission, so just pick one if it hasn't been set.
- var testerpackage = new byte[]{ (byte) (salt >> 56), (byte) (salt >> 48), (byte) (salt >> 40), (byte) (salt >> 32), (byte) (salt >> 24), (byte) (salt >> 16), (byte) (salt >> 8), (byte) salt };
- lastSubmission = jobs.submit(applicationId, new Submission(applicationPackage, testerpackage, Optional.empty(), sourceRevision, Optional.of("a@b"), Optional.empty(), tester.clock().instant(), risk), projectId).id();
- return this;
- }
-
-
- /** Submit the default application package for deployment */
- public DeploymentContext submit() {
- return submit(tester.controller().system().isPublic() ? publicApplicationPackage() : applicationPackage());
- }
-
- /** Trigger all outstanding jobs, if any */
- public DeploymentContext triggerJobs() {
- tester.triggerJobs();
- return this;
- }
-
- /** Fail current deployment in given job */
- public DeploymentContext nodeAllocationFailure(JobType type) {
- return failDeployment(type,
- new ConfigServerException(ErrorCode.NODE_ALLOCATION_FAILURE,
- "Node allocation failure",
- "Failed to deploy application"));
- }
-
- /** Fail current deployment in given job */
- public DeploymentContext failDeployment(JobType type) {
- return failDeployment(type, new RuntimeException("Exception from test code"));
- }
-
- /** Fail current deployment in given job */
- private DeploymentContext failDeployment(JobType type, RuntimeException exception) {
- configServer().throwOnNextPrepare(exception);
- runJobExpectingFailure(type, null);
- return this;
- }
-
- /** Run given job and expect it to fail with given message, if any */
- public DeploymentContext runJobExpectingFailure(JobType type, String messagePart) {
- triggerJobs();
- var job = jobId(type);
- RunId id = currentRun(job).id();
- runner.advance(currentRun(job));
- Run run = jobs.run(id);
- assertTrue(run.hasFailed());
- assertTrue(run.hasEnded());
- if (messagePart != null) {
- Optional<Step> firstFailing = run.stepStatuses().entrySet().stream()
- .filter(kv -> kv.getValue() == failed)
- .map(Entry::getKey)
- .findFirst();
- assertTrue(firstFailing.isPresent(), "Found failing step");
- Optional<RunLog> details = jobs.details(id);
- assertTrue(details.isPresent(), "Found log entries for run " + id);
- assertTrue(details.get().get(firstFailing.get()).stream()
- .anyMatch(entry -> entry.message().contains(messagePart)),
- "Found log message containing '" + messagePart + "'");
- }
- return this;
- }
-
- /** Returns the last submitted application version */
- public Optional<RevisionId> lastSubmission() {
- return Optional.ofNullable(lastSubmission);
- }
-
- public DeploymentContext completeRollout() {
- return completeRollout(false);
- }
-
- /** Runs and returns all remaining jobs for the application, at most once, and asserts the current change is rolled out. */
- public DeploymentContext completeRollout(boolean multiInstance) {
- triggerJobs();
- Map<ApplicationId, Map<JobType, Run>> runsByInstance = new HashMap<>();
- List<Run> activeRuns;
- while ( ! (activeRuns = this.jobs.active(applicationId)).isEmpty())
- for (Run run : activeRuns) {
- Map<JobType, Run> runs = runsByInstance.computeIfAbsent(run.id().application(), k -> new HashMap<>());
- Run previous = runs.put(run.id().type(), run);
- if (previous != null && run.versions().equals(previous.versions()) && run.id().type().zone().equals(previous.id().type().zone())) {
- throw new AssertionError("Job '" + run.id() + "' was run twice on same versions");
- }
- runJob(run.id().type(), run.id().application());
- if (multiInstance) {
- tester.outstandingChangeDeployer().run();
- }
- triggerJobs();
- }
-
- assertFalse(instance().change().hasTargets(), "Change should have no targets, but was " + instance().change());
- return this;
- }
-
- /** Runs a deployment of the given package to the given dev/perf job, on the given version. */
- public DeploymentContext runJob(JobType type, ApplicationPackage applicationPackage, Version vespaVersion) {
- jobs.deploy(instanceId, type, Optional.ofNullable(vespaVersion), applicationPackage, false, true);
- return runJob(type);
- }
-
- /** Runs a deployment of the given package to the given manually deployable job. */
- public DeploymentContext runJob(JobType type, ApplicationPackage applicationPackage) {
- return runJob(type, applicationPackage, null);
- }
-
- /** Runs a deployment of the given package to the given manually deployable zone. */
- public DeploymentContext runJob(ZoneId zone, ApplicationPackage applicationPackage) {
- return runJob(JobType.deploymentTo(zone), applicationPackage, null);
- }
-
- /** Pulls the ready job trigger, and then runs the whole of the given job in the instance of this, successfully. */
- public DeploymentContext runJob(JobType type) {
- return runJob(type, instanceId);
- }
-
- /** Runs the job, failing tests with noTests status, or with regular testFailure. */
- public DeploymentContext failTests(JobType type, boolean noTests) {
- if ( ! type.isTest()) throw new IllegalArgumentException(type + " does not run tests");
- var job = new JobId(instanceId, type);
- triggerJobs();
- doDeploy(job);
- if (job.type().isDeployment()) {
- doUpgrade(job);
- doConverge(job);
- if (job.type().environment().isManuallyDeployed())
- return this;
- }
-
- RunId id = currentRun(job).id();
-
- assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.endTests));
- tester.cloud().set(noTests ? Status.NO_TESTS : Status.FAILURE);
- runner.advance(currentRun(job));
- assertTrue(jobs.run(id).hasEnded());
- assertTrue(configServer().nodeRepository().list(job.type().zone(), NodeFilter.all().applications(TesterId.of(instanceId).id())).isEmpty());
-
- return this;
- }
-
- /** Pulls the ready job trigger, and then runs the whole of job for the given instance, successfully. */
- private DeploymentContext runJob(JobType type, ApplicationId instance) {
- triggerJobs();
- Run run = currentRun(new JobId(instance, type));
- assertEquals(type.zone(), run.id().type().zone());
- JobId job = run.id().job();
- doDeploy(job);
- if (job.type().isDeployment()) {
- doUpgrade(job);
- doConverge(job);
- if (job.type().environment().isManuallyDeployed())
- return this;
- }
- if (job.type().isTest())
- doTests(job);
- return this;
- }
-
- /** Abort the running job of the given type. */
- public DeploymentContext abortJob(JobType type) {
- var job = jobId(type);
- assertNotSame(RunStatus.aborted, currentRun(job).status());
- jobs.abort(currentRun(job).id(), "DeploymentContext.abortJob", false);
- jobAborted(type);
- return this;
- }
-
- /** Finish an already aborted run of the given type. */
- public DeploymentContext jobAborted(JobType type) {
- Run run = jobs.last(instanceId, type).get();
- assertSame(RunStatus.aborted, run.status());
- assertFalse(run.hasEnded());
- runner.advance(run);
- assertTrue(jobs.run(run.id()).hasEnded());
- return this;
- }
-
- /** Simulate upgrade time out in given job */
- public DeploymentContext timeOutUpgrade(JobType type) {
- var job = jobId(type);
- triggerJobs();
- RunId id = currentRun(job).id();
- doDeploy(job);
- tester.clock().advance(Timeouts.of(tester.controller().system()).noNodesDown().plusSeconds(1));
- runner.advance(currentRun(job));
- assertTrue(jobs.run(id).hasFailed());
- assertTrue(jobs.run(id).hasEnded());
- return this;
- }
-
- /** Simulate convergence time out in given job */
- public DeploymentContext timeOutConvergence(JobType type) {
- var job = jobId(type);
- triggerJobs();
- RunId id = currentRun(job).id();
- doDeploy(job);
- doUpgrade(job);
- tester.clock().advance(Timeouts.of(tester.controller().system()).noNodesDown().plusSeconds(1));
- runner.advance(currentRun(job));
- assertTrue(jobs.run(id).hasFailed());
- assertTrue(jobs.run(id).hasEnded());
- return this;
- }
-
- /** Deploy default application package, start a run for that change and return its ID */
- public RunId newRun(JobType type) {
- submit();
- tester.readyJobsTrigger().maintain();
-
- if (type.isProduction()) {
- runJob(systemTest);
- runJob(stagingTest);
- tester.readyJobsTrigger().maintain();
- }
-
- Run run = jobs.active().stream()
- .filter(r -> r.id().type().equals(type))
- .findAny()
- .orElseThrow(() -> new AssertionError(type + " is not among the active: " + jobs.active()));
- return run.id();
- }
-
- /** Start tests in system test stage */
- public RunId startSystemTestTests() {
- var id = newRun(systemTest);
- var testZone = systemTest.zone();
- runner.run();
- if ( ! deferDnsUpdates)
- flushDnsUpdates();
- configServer().convergeServices(instanceId, testZone);
- configServer().convergeServices(testerId.id(), testZone);
- runner.run();
- assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.endTests));
- assertTrue(jobs.run(id).steps().get(Step.endTests).startTime().isPresent());
- return id;
- }
-
- public void assertRunning(JobType type) {
- assertTrue(jobs.active().stream().anyMatch(run -> run.id().application().equals(instanceId) && run.id().type().equals(type)),
- jobId(type) + " should be among the active: " + jobs.active());
- }
-
- public void assertNotRunning(JobType type) {
- assertFalse(jobs.active().stream().anyMatch(run -> run.id().application().equals(instanceId) && run.id().type().equals(type)),
- jobId(type) + " should not be among the active: " + jobs.active());
- }
-
- /** Deploys tester and real app, and completes tester and initial staging installation first if needed. */
- private void doDeploy(JobId job) {
- RunId id = currentRun(job).id();
- ZoneId zone = job.type().zone();
- DeploymentId deployment = new DeploymentId(job.application(), zone);
-
- // First step is always a deployment.
- runner.advance(currentRun(job));
-
- if ( ! deferDnsUpdates)
- flushDnsUpdates();
-
- if (job.type().isTest())
- doInstallTester(job);
-
- if (job.type().equals(stagingTest)) { // Do the initial deployment and installation of the real application.
- assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.installInitialReal));
- tester.configServer().nodeRepository().doUpgrade(deployment, Optional.empty(), tester.configServer().application(job.application(), zone).get().version().get());
- configServer().convergeServices(id.application(), zone);
- runner.advance(currentRun(job));
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installInitialReal));
-
- // All installation is complete and endpoints are ready, so setup may begin.
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installInitialReal));
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installTester));
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.startStagingSetup));
-
- assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.endStagingSetup));
- tester.cloud().set(Status.SUCCESS);
- runner.advance(currentRun(job));
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.endStagingSetup));
-
- if ( ! deferDnsUpdates)
- flushDnsUpdates();
- }
- }
-
- /** Upgrades nodes to target version. */
- private void doUpgrade(JobId job) {
- RunId id = currentRun(job).id();
- ZoneId zone = job.type().zone();
- DeploymentId deployment = new DeploymentId(job.application(), zone);
-
- assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.installReal));
- configServer().nodeRepository().doUpgrade(deployment, Optional.empty(), tester.configServer().application(job.application(), zone).get().version().get());
- runner.advance(currentRun(job));
- }
-
- /** Returns the current run for the given job type, and verifies it is still running normally. */
- private Run currentRun(JobId job) {
- Run run = jobs.last(job)
- .filter(r -> r.id().type().equals(job.type()))
- .orElseThrow(() -> new AssertionError(job.type() + " is not among the active: " + jobs.active()));
- assertFalse(run.hasFailed(), run.id() + " should not have failed yet: " + run);
- assertFalse(run.hasEnded(), run.id() + " should not have ended yet: " + run);
- return run;
- }
-
- /** Lets nodes converge on new application version. */
- private void doConverge(JobId job) {
- RunId id = currentRun(job).id();
- ZoneId zone = job.type().zone();
-
- assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.installReal));
- configServer().convergeServices(id.application(), zone);
- runner.advance(currentRun(job));
- if (job.type().environment().isManuallyDeployed()) {
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installReal));
- assertTrue(jobs.run(id).hasEnded());
- return;
- }
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installReal), "Status of " + id);
- }
-
- /** Installs tester and starts tests. */
- private void doInstallTester(JobId job) {
- RunId id = currentRun(job).id();
- ZoneId zone = job.type().zone();
-
- assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.installTester));
- configServer().nodeRepository().doUpgrade(new DeploymentId(TesterId.of(job.application()).id(), zone), Optional.empty(), tester.configServer().application(id.tester().id(), zone).get().version().get());
- runner.advance(currentRun(job));
- assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.installTester));
- configServer().convergeServices(TesterId.of(id.application()).id(), zone);
- runner.advance(currentRun(job));
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installTester));
- runner.advance(currentRun(job));
- }
-
- /** Completes tests with success. */
- private void doTests(JobId job) {
- RunId id = currentRun(job).id();
- ZoneId zone = job.type().zone();
-
- // All installation is complete and endpoints are ready, so tests may begin.
- if (job.type().isDeployment())
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installReal));
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.installTester));
- assertEquals(succeeded, jobs.run(id).stepStatuses().get(Step.startTests));
-
- assertEquals(unfinished, jobs.run(id).stepStatuses().get(Step.endTests));
- tester.cloud().set(Status.SUCCESS);
- runner.advance(currentRun(job));
- assertTrue(jobs.run(id).hasEnded());
- assertFalse(jobs.run(id).hasFailed());
- Instance instance = tester.application(TenantAndApplicationId.from(instanceId)).require(id.application().instance());
- assertEquals(job.type().isProduction(), instance.deployments().containsKey(zone));
- assertTrue(configServer().nodeRepository().list(zone, NodeFilter.all().applications(TesterId.of(instance.id()).id())).isEmpty());
- }
-
- private JobId jobId(JobType type) {
- return new JobId(instanceId, type);
- }
-
- private ConfigServerMock configServer() {
- return tester.configServer();
- }
-
- private static X509Certificate generateCertificate() {
- KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
- X500Principal subject = new X500Principal("CN=subject");
- return X509CertificateBuilder.fromKeypair(keyPair,
- subject,
- Instant.now(),
- Instant.now().plusSeconds(1),
- SignatureAlgorithm.SHA512_WITH_ECDSA,
- BigInteger.valueOf(1))
- .build();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
deleted file mode 100644
index bc03d46a30a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTester.java
+++ /dev/null
@@ -1,154 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.test.ManualClock;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockTesterCloud;
-import com.yahoo.vespa.hosted.controller.application.ApplicationList;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock;
-import com.yahoo.vespa.hosted.controller.maintenance.JobRunner;
-import com.yahoo.vespa.hosted.controller.maintenance.JobRunnerTest;
-import com.yahoo.vespa.hosted.controller.maintenance.OutstandingChangeDeployer;
-import com.yahoo.vespa.hosted.controller.maintenance.ReadyJobsTrigger;
-import com.yahoo.vespa.hosted.controller.maintenance.Upgrader;
-
-import java.time.DayOfWeek;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.ZoneOffset;
-import java.time.temporal.TemporalAdjusters;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author jonmv
- */
-public class DeploymentTester {
-
- // Set a long interval so that maintainers never do scheduled runs during tests
- private static final Duration maintenanceInterval = Duration.ofDays(1);
-
- private static final String ATHENZ_DOMAIN = "domain";
- private static final String ATHENZ_SERVICE = "service";
-
- public static final TenantAndApplicationId appId = TenantAndApplicationId.from("tenant", "application");
- public static final ApplicationId instanceId = appId.defaultInstance();
-
- private final ControllerTester tester;
- private final JobController jobs;
- private final MockTesterCloud cloud;
- private final JobRunner runner;
- private final Upgrader upgrader;
- private final ReadyJobsTrigger readyJobsTrigger;
- private final OutstandingChangeDeployer outstandingChangeDeployer;
-
- public JobController jobs() { return jobs; }
- public MockTesterCloud cloud() { return cloud; }
- public JobRunner runner() { return runner; }
- public ConfigServerMock configServer() { return tester.configServer(); }
- public Controller controller() { return tester.controller(); }
- public DeploymentTrigger deploymentTrigger() { return applications().deploymentTrigger(); }
- public ControllerTester controllerTester() { return tester; }
- public Upgrader upgrader() { return upgrader; }
- public ApplicationController applications() { return tester.controller().applications(); }
- public ManualClock clock() { return tester.clock(); }
- public Application application() { return application(appId); }
- public Application application(TenantAndApplicationId id ) { return applications().requireApplication(id); }
- public Instance instance() { return instance(instanceId); }
- public Instance instance(ApplicationId id) { return applications().requireInstance(id); }
- public DeploymentStatusList deploymentStatuses() { return jobs.deploymentStatuses(ApplicationList.from(applications().asList())); }
-
- public DeploymentTester() {
- this(new ControllerTester());
- }
-
- public DeploymentTester(ControllerTester controllerTester) {
- tester = controllerTester;
- jobs = tester.controller().jobController();
- cloud = (MockTesterCloud) tester.controller().jobController().cloud();
- runner = new JobRunner(tester.controller(), maintenanceInterval, JobRunnerTest.inThreadInOrderExecutor(), new InternalStepRunner(tester.controller()));
- upgrader = new Upgrader(tester.controller(), maintenanceInterval);
- upgrader.setUpgradesPerMinute(1); // Anything that makes it at least one for any maintenance period is fine.
- readyJobsTrigger = new ReadyJobsTrigger(tester.controller(), maintenanceInterval);
- outstandingChangeDeployer = new OutstandingChangeDeployer(tester.controller(), maintenanceInterval);
-
- // Get deployment job logs to stderr.
- Logger.getLogger("").setLevel(Level.FINE);
- Logger.getLogger(InternalStepRunner.class.getName()).setLevel(Level.FINE);
- tester.configureDefaultLogHandler(handler -> handler.setLevel(Level.FINE));
-
- // Mock Athenz domain to allow launch of service
- AthenzDbMock.Domain domain = tester.athenzDb().getOrCreateDomain(new com.yahoo.vespa.athenz.api.AthenzDomain(ATHENZ_DOMAIN));
- domain.services.put(ATHENZ_SERVICE, new AthenzDbMock.Service(true));
- }
-
- public ReadyJobsTrigger readyJobsTrigger() {
- return readyJobsTrigger;
- }
-
- public OutstandingChangeDeployer outstandingChangeDeployer() { return outstandingChangeDeployer; }
-
- /** A tester with clock configured to a time when confidence can freely change */
- public DeploymentTester atMondayMorning() {
- return at(tester.clock().instant().atZone(ZoneOffset.UTC)
- .with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY))
- .withHour(5)
- .toInstant());
- }
-
- public DeploymentTester at(Instant instant) {
- tester.clock().setInstant(instant);
- return this;
- }
-
- /** Create the deployment context for the default instance id */
- public DeploymentContext newDeploymentContext() {
- return newDeploymentContext(instanceId);
- }
-
- /** Create a new deployment context for given application */
- public DeploymentContext newDeploymentContext(String tenantName, String applicationName, String instanceName) {
- return newDeploymentContext(ApplicationId.from(tenantName, applicationName, instanceName));
- }
-
- /** Create a new deployment context for given application */
- public DeploymentContext newDeploymentContext(ApplicationId instance) {
- return new DeploymentContext(instance, this);
- }
-
- /** Create a new application with given tenant and application name */
- public Application createApplication(String tenantName, String applicationName, String instanceName) {
- return newDeploymentContext(tenantName, applicationName, instanceName).application();
- }
-
- /** Aborts and finishes all running jobs. */
- public void abortAll() {
- triggerJobs();
- for (Run run : jobs.active()) {
- jobs.abort(run.id(), "DeploymentTester.abortAll", false);
- runner.advance(jobs.run(run.id()));
- assertTrue(jobs.run(run.id()).hasEnded());
- }
- }
-
- /** Triggers jobs until nothing more triggers, and returns the number of triggered jobs. */
- public int triggerJobs() {
- int triggered;
- int triggeredTotal = 0;
- do {
- triggered = (int) deploymentTrigger().triggerReadyJobs().triggered();
- triggeredTotal += triggered;
- } while (triggered > 0);
- return triggeredTotal;
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
deleted file mode 100644
index a6ea8fa074e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/DeploymentTriggerTest.java
+++ /dev/null
@@ -1,3146 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException.ErrorCode;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock;
-import com.yahoo.vespa.hosted.controller.maintenance.DeploymentUpgrader;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalLong;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static ai.vespa.validation.Validation.require;
-import static com.yahoo.config.provision.Environment.prod;
-import static com.yahoo.config.provision.SystemName.cd;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.applicationPackage;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionApNortheast1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionApNortheast2;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionApSoutheast1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionAwsUsEast1a;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionEuWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsCentral1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsEast3;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.stagingTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.systemTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.testApNortheast1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.testApNortheast2;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.testEuWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.testUsCentral1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.testUsEast3;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.testUsWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.ALL;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.PLATFORM;
-import static java.util.Collections.emptyList;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * Tests a wide variety of deployment scenarios and configurations
- *
- * @author bratseth
- * @author mpolden
- * @author jonmv
- */
-public class DeploymentTriggerTest {
-
- private final DeploymentTester tester = new DeploymentTester();
-
- @Test
- void testTriggerFailing() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .upgradePolicy("default")
- .region("us-west-1")
- .build();
-
- // Deploy completely once
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- // New version is released
- Version version = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version);
- tester.upgrader().maintain();
-
- // staging-test fails deployment and is retried
- app.failDeployment(stagingTest);
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size(), "Retried dead job");
- app.assertRunning(stagingTest);
- app.runJob(stagingTest);
-
- // system-test is now the only running job -- production jobs haven't started yet, since it is unfinished.
- app.assertRunning(systemTest);
- assertEquals(1, tester.jobs().active().size());
-
- // system-test fails and is retried
- app.timeOutUpgrade(systemTest);
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size(), "Job is retried on failure");
- app.runJob(systemTest);
-
- tester.triggerJobs();
- app.assertRunning(productionUsWest1);
-
- tester.jobs().abort(tester.jobs().last(app.instanceId(), productionUsWest1).get().id(), "cancelled", true);
- tester.runner().run();
- assertEquals(RunStatus.cancelled, tester.jobs().last(app.instanceId(), productionUsWest1).get().status());
- tester.triggerJobs();
- app.assertNotRunning(productionUsWest1);
- tester.deploymentTrigger().reTrigger(app.instanceId(), productionUsWest1, "retry");
- app.assertRunning(productionUsWest1);
-
- // invalid application is not retried
- tester.configServer().throwOnNextPrepare(new ConfigServerException(ErrorCode.INVALID_APPLICATION_PACKAGE, "nope", "bah"));
- tester.runner().run();
- assertEquals(RunStatus.invalidApplication, tester.jobs().last(app.instanceId(), productionUsWest1).get().status());
- tester.triggerJobs();
- app.assertNotRunning(productionUsWest1);
-
- // production-us-west-1 fails, but the app loses its projectId, and the job isn't retried.
- app.submit(applicationPackage).runJob(systemTest).runJob(stagingTest).triggerJobs();
- tester.applications().lockApplicationOrThrow(app.application().id(), locked ->
- tester.applications().store(locked.withProjectId(OptionalLong.empty())));
-
- app.timeOutConvergence(productionUsWest1);
- tester.triggerJobs();
- assertEquals(0, tester.jobs().active().size(), "Job is not triggered when no projectId is present");
- }
-
- @Test
- void revisionChangeWhenFailingMakesApplicationChangeWaitForPreviousToComplete() {
- DeploymentContext app = tester.newDeploymentContext();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .revisionChange(null) // separate by default, but we override this in test builder
- .region("us-east-3")
- .test("us-east-3")
- .build();
-
- app.submit(applicationPackage).runJob(systemTest).runJob(stagingTest).runJob(productionUsEast3);
- Optional<RevisionId> v0 = app.lastSubmission();
-
- app.submit(applicationPackage);
- Optional<RevisionId> v1 = app.lastSubmission();
- assertEquals(v0, app.instance().change().revision());
-
- // Eager tests still run before new revision rolls out.
- app.runJob(systemTest).runJob(stagingTest);
-
- // v0 rolls out completely.
- app.runJob(testUsEast3);
- assertEquals(Optional.empty(), app.instance().change().revision());
-
- // v1 starts rolling when v0 is done.
- tester.outstandingChangeDeployer().run();
- assertEquals(v1, app.instance().change().revision());
-
- // v1 fails, so v2 starts immediately.
- app.runJob(productionUsEast3).failDeployment(testUsEast3);
- app.submit(applicationPackage);
- Optional<RevisionId> v2 = app.lastSubmission();
- assertEquals(v2, app.instance().change().revision());
- }
-
- @Test
- void leadingUpgradeAllowsApplicationChangeWhileUpgrading() {
- var applicationPackage = new ApplicationPackageBuilder().region("us-east-3")
- .upgradeRollout("leading")
- .build();
- var app = tester.newDeploymentContext();
-
- app.submit(applicationPackage).deploy();
-
- Change upgrade = Change.of(new Version("6.8.9"));
- tester.controllerTester().upgradeSystem(upgrade.platform().get());
- tester.upgrader().maintain();
- app.runJob(systemTest).runJob(stagingTest);
- tester.triggerJobs();
- app.assertRunning(productionUsEast3);
- assertEquals(upgrade, app.instance().change());
-
- app.submit(applicationPackage);
- assertEquals(upgrade.with(app.lastSubmission().get()), app.instance().change());
- }
-
- @Test
- void abortsJobsOnNewApplicationChange() {
- var app = tester.newDeploymentContext();
- app.submit()
- .runJob(systemTest)
- .runJob(stagingTest);
-
- tester.triggerJobs();
- RunId id = tester.jobs().last(app.instanceId(), productionUsCentral1).get().id();
- assertTrue(tester.jobs().active(id).isPresent());
-
- app.submit();
- assertTrue(tester.jobs().active(id).isPresent());
-
- tester.triggerJobs();
- tester.runner().run();
- assertTrue(tester.jobs().active(id).isPresent()); // old run
-
- app.runJob(systemTest).runJob(stagingTest).runJob(stagingTest); // outdated run is aborted when otherwise blocking a new run
- tester.triggerJobs();
- app.jobAborted(productionUsCentral1);
- Versions outdated = tester.jobs().last(app.instanceId(), productionUsCentral1).get().versions();
-
- // Flesh bag re-triggers job, and _that_ is not aborted
- tester.deploymentTrigger().reTrigger(app.instanceId(), productionUsCentral1, "flesh bag");
- tester.triggerJobs();
- app.runJob(productionUsCentral1);
- Versions reTriggered = tester.jobs().last(app.instanceId(), productionUsCentral1).get().versions();
- assertEquals(outdated, reTriggered);
-
- app.runJob(productionUsCentral1).runJob(productionUsWest1).runJob(productionUsEast3);
- assertEquals(Change.empty(), app.instance().change());
-
- tester.controllerTester().upgradeSystem(new Version("6.9"));
- tester.upgrader().maintain();
- app.runJob(systemTest).runJob(stagingTest);
- tester.clock().advance(Duration.ofMinutes(1));
- tester.triggerJobs();
-
- // Upgrade is allowed to proceed ahead of revision change, and is not aborted.
- app.submit();
- app.runJob(systemTest).runJob(stagingTest);
- tester.triggerJobs();
- tester.runner().run();
- assertEquals(Set.of(productionUsCentral1), tester.jobs().active().stream()
- .map(run -> run.id().type())
- .collect(Collectors.toCollection(HashSet::new)));
- }
-
- @Test
- void similarDeploymentSpecsAreNotRolledOut() {
- ApplicationPackage firstPackage = new ApplicationPackageBuilder()
- .region("us-east-3")
- .build();
-
- DeploymentContext app = tester.newDeploymentContext().submit(firstPackage, 5417, 0);
- var version = app.lastSubmission();
- assertEquals(version, app.instance().change().revision());
- app.runJob(systemTest)
- .runJob(stagingTest)
- .runJob(productionUsEast3);
- assertEquals(Change.empty(), app.instance().change());
-
- // A similar application package is submitted. Since a new job is added, the original revision is again a target.
- ApplicationPackage secondPackage = new ApplicationPackageBuilder()
- .systemTest()
- .stagingTest()
- .region("us-east-3")
- .delay(Duration.ofHours(1))
- .test("us-east-3")
- .build();
-
- app.submit(secondPackage, 5417, 0);
- app.triggerJobs();
- assertEquals(List.of(), tester.jobs().active());
- assertEquals(version, app.instance().change().revision());
-
- tester.clock().advance(Duration.ofHours(1));
- app.runJob(testUsEast3);
- assertEquals(List.of(), tester.jobs().active());
- assertEquals(Change.empty(), app.instance().change());
-
- // The original application package is submitted again. No new jobs are added, so no change needs to roll out now.
- app.submit(firstPackage, 5417, 0);
- app.triggerJobs();
- assertEquals(List.of(), tester.jobs().active());
- assertEquals(Change.empty(), app.instance().change());
- }
-
- @Test
- void testOutstandingChangeWithNextRevisionTarget() {
- ApplicationPackage appPackage = new ApplicationPackageBuilder().revisionTarget("next")
- .revisionChange("when-failing")
- .region("us-east-3")
- .build();
- DeploymentContext app = tester.newDeploymentContext()
- .submit(appPackage);
- Optional<RevisionId> revision1 = app.lastSubmission();
-
- app.submit(appPackage);
- Optional<RevisionId> revision2 = app.lastSubmission();
-
- app.submit(appPackage);
- Optional<RevisionId> revision3 = app.lastSubmission();
-
- app.submit(appPackage);
- Optional<RevisionId> revision4 = app.lastSubmission();
-
- app.submit(appPackage);
- Optional<RevisionId> revision5 = app.lastSubmission();
-
- // 5 revisions submitted; the first is rolling out, and the others are queued.
- tester.outstandingChangeDeployer().run();
- assertEquals(revision1, app.instance().change().revision());
- assertEquals(revision2, app.deploymentStatus().outstandingChange(InstanceName.defaultName()).revision());
-
- // The second revision is set as the target by user interaction.
- tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(revision2.get()));
- tester.outstandingChangeDeployer().run();
- assertEquals(revision2, app.instance().change().revision());
- assertEquals(revision3, app.deploymentStatus().outstandingChange(InstanceName.defaultName()).revision());
-
- // The second revision deploys completely, and the third starts rolling out.
- app.runJob(systemTest).runJob(stagingTest)
- .runJob(productionUsEast3);
- tester.outstandingChangeDeployer().run();
- tester.outstandingChangeDeployer().run();
- assertEquals(revision3, app.instance().change().revision());
- assertEquals(revision4, app.deploymentStatus().outstandingChange(InstanceName.defaultName()).revision());
-
- // The third revision fails, and the fourth is chosen to replace it.
- app.triggerJobs().timeOutConvergence(systemTest);
- tester.outstandingChangeDeployer().run();
- tester.outstandingChangeDeployer().run();
- assertEquals(revision4, app.instance().change().revision());
- assertEquals(revision5, app.deploymentStatus().outstandingChange(InstanceName.defaultName()).revision());
-
- // Tests for outstanding change are relevant when current revision completes.
- app.runJob(systemTest).runJob(systemTest)
- .jobAborted(stagingTest).runJob(stagingTest).runJob(stagingTest)
- .runJob(productionUsEast3);
- tester.outstandingChangeDeployer().run();
- tester.outstandingChangeDeployer().run();
- assertEquals(revision5, app.instance().change().revision());
- assertEquals(Change.empty(), app.deploymentStatus().outstandingChange(InstanceName.defaultName()));
- app.runJob(productionUsEast3);
- }
-
- @Test
- void deploymentSpecWithDelays() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .systemTest()
- .delay(Duration.ofSeconds(30))
- .region("us-west-1")
- .delay(Duration.ofMinutes(2))
- .delay(Duration.ofMinutes(2)) // Multiple delays are summed up
- .region("us-central-1")
- .delay(Duration.ofMinutes(10)) // Delays after last region are valid, but have no effect
- .build();
- var app = tester.newDeploymentContext().submit(applicationPackage);
-
- // Test jobs pass
- app.runJob(systemTest);
- tester.clock().advance(Duration.ofSeconds(15));
- app.runJob(stagingTest);
- tester.triggerJobs();
-
- // No jobs have started yet, as 30 seconds have not yet passed.
- assertEquals(0, tester.jobs().active().size());
- tester.clock().advance(Duration.ofSeconds(15));
- tester.triggerJobs();
-
- // 30 seconds after the declared test, jobs may begin. The implicit test does not affect the delay.
- assertEquals(1, tester.jobs().active().size());
- app.assertRunning(productionUsWest1);
-
- // 3 minutes pass, delayed trigger does nothing as us-west-1 is still in progress
- tester.clock().advance(Duration.ofMinutes(3));
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size());
- app.assertRunning(productionUsWest1);
-
- // us-west-1 completes
- app.runJob(productionUsWest1);
-
- // Delayed trigger does nothing as not enough time has passed after us-west-1 completion
- tester.triggerJobs();
- assertTrue(tester.jobs().active().isEmpty(), "No more jobs triggered at this time");
-
- // 3 minutes pass, us-central-1 is still not triggered
- tester.clock().advance(Duration.ofMinutes(3));
- tester.triggerJobs();
- assertTrue(tester.jobs().active().isEmpty(), "No more jobs triggered at this time");
-
- // 4 minutes pass, us-central-1 is triggered
- tester.clock().advance(Duration.ofMinutes(1));
- tester.triggerJobs();
- app.runJob(productionUsCentral1);
- assertTrue(tester.jobs().active().isEmpty(), "All jobs consumed");
-
- // Delayed trigger job runs again, with nothing to trigger
- tester.clock().advance(Duration.ofMinutes(10));
- tester.triggerJobs();
- assertTrue(tester.jobs().active().isEmpty(), "All jobs consumed");
- }
-
- @Test
- void deploymentSpecWithParallelDeployments() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-central-1")
- .parallel("us-west-1", "us-east-3")
- .region("eu-west-1")
- .build();
-
- var app = tester.newDeploymentContext().submit(applicationPackage);
-
- // Test jobs pass
- app.runJob(systemTest).runJob(stagingTest);
-
- // Deploys in first region
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size());
- app.runJob(productionUsCentral1);
-
- // Deploys in two regions in parallel
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size());
- app.assertRunning(productionUsEast3);
- app.assertRunning(productionUsWest1);
-
- app.runJob(productionUsWest1);
- assertEquals(1, tester.jobs().active().size());
- app.assertRunning(productionUsEast3);
-
- app.runJob(productionUsEast3);
-
- // Last region completes
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size());
- app.runJob(productionEuWest1);
- assertTrue(tester.jobs().active().isEmpty(), "All jobs consumed");
- }
-
- @Test
- void testNoOtherChangesDuringSuspension() {
- // Application is deployed in 3 regions:
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-central-1")
- .parallel("us-west-1", "us-east-3")
- .build();
- var application = tester.newDeploymentContext().submit().deploy();
-
- // The first production zone is suspended:
- tester.configServer().setSuspension(application.deploymentIdIn(ZoneId.from("prod", "us-central-1")), true);
-
- // A new change needs to be pushed out, but should not go beyond the suspended zone:
- application.submit()
- .runJob(systemTest)
- .runJob(stagingTest)
- .runJob(productionUsCentral1);
- tester.triggerJobs();
- application.assertNotRunning(productionUsEast3);
- application.assertNotRunning(productionUsWest1);
-
- // The zone is unsuspended so jobs start:
- tester.configServer().setSuspension(application.deploymentIdIn(ZoneId.from("prod", "us-central-1")), false);
- tester.triggerJobs();
- application.runJob(productionUsWest1).runJob(productionUsEast3);
- assertEquals(Change.empty(), application.instance().change());
- }
-
- @Test
- void testBlockRevisionChange() {
- // Tuesday, 17:30
- tester.at(Instant.parse("2017-09-26T17:30:00.00Z"));
-
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
-
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .upgradePolicy("canary")
- // Block application version changes on tuesday in hours 18 and 19
- .blockChange(true, false, "tue", "18-19", "UTC")
- .region("us-west-1")
- .region("us-central-1")
- .region("us-east-3")
- .build();
-
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- tester.clock().advance(Duration.ofHours(1)); // --------------- Enter block window: 18:30
-
- tester.triggerJobs();
- assertEquals(0, tester.jobs().active().size());
-
- app.submit(applicationPackage);
- assertTrue(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets());
- app.runJob(systemTest).runJob(stagingTest);
-
- tester.outstandingChangeDeployer().run();
- assertTrue(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets());
-
- tester.triggerJobs();
- assertEquals(emptyList(), tester.jobs().active());
-
- tester.clock().advance(Duration.ofHours(2)); // ---------------- Exit block window: 20:30
-
- tester.outstandingChangeDeployer().run();
- assertFalse(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets());
-
- tester.triggerJobs(); // Tests already run for the blocked production job.
- app.assertRunning(productionUsWest1);
- }
-
- @Test
- void testCompletionOfPartOfChangeDuringBlockWindow() {
- // Tuesday, 17:30
- tester.at(Instant.parse("2017-09-26T17:30:00.00Z"));
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .blockChange(true, true, "tue", "18", "UTC")
- .region("us-west-1")
- .region("us-east-3")
- .build();
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- // Application on (6.1, 1.0.1)
- Version v1 = Version.fromString("6.1");
-
- // Application is mid-upgrade when block window begins, and gets an outstanding change.
- Version v2 = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(v2);
- tester.upgrader().maintain();
- app.runJob(stagingTest).runJob(systemTest);
-
- // Entering block window will keep the outstanding change in place.
- tester.clock().advance(Duration.ofHours(1));
- app.submit(applicationPackage);
- app.runJob(productionUsWest1);
- assertEquals(1, app.instanceJobs().get(productionUsWest1).lastSuccess().get().versions().targetRevision().number());
- assertEquals(2, app.deploymentStatus().outstandingChange(app.instance().name()).revision().get().number());
-
- tester.triggerJobs();
- // Platform upgrade keeps rolling, since it began before block window, and tests for the new revision have also started.
- assertEquals(3, tester.jobs().active().size());
- app.runJob(productionUsEast3);
- assertEquals(2, tester.jobs().active().size());
-
- // Upgrade is done, and outstanding change rolls out when block window ends.
- assertEquals(Change.empty(), app.instance().change());
- assertTrue(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets());
-
- app.runJob(stagingTest).runJob(systemTest);
- tester.clock().advance(Duration.ofHours(1));
- tester.outstandingChangeDeployer().run();
- assertTrue(app.instance().change().hasTargets());
- assertFalse(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets());
-
- app.runJob(productionUsWest1).runJob(productionUsEast3);
-
- assertFalse(app.instance().change().hasTargets());
- }
-
- @Test
- void testJobPause() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .region("us-east-3")
- .build();
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
- tester.controllerTester().upgradeSystem(new Version("6.8.7"));
- tester.upgrader().maintain();
-
- tester.deploymentTrigger().pauseJob(app.instanceId(), productionUsWest1,
- tester.clock().instant().plus(Duration.ofSeconds(1)));
- tester.deploymentTrigger().pauseJob(app.instanceId(), productionUsEast3,
- tester.clock().instant().plus(Duration.ofSeconds(3)));
-
- // us-west-1 does not trigger when paused.
- app.runJob(systemTest).runJob(stagingTest);
- tester.triggerJobs();
- app.assertNotRunning(productionUsWest1);
-
- // us-west-1 triggers when no longer paused, but does not retry when paused again.
- tester.clock().advance(Duration.ofMillis(1500));
- tester.triggerJobs();
- app.assertRunning(productionUsWest1);
- tester.deploymentTrigger().pauseJob(app.instanceId(), productionUsWest1, tester.clock().instant().plus(Duration.ofSeconds(1)));
- app.failDeployment(productionUsWest1);
- tester.triggerJobs();
- app.assertNotRunning(productionUsWest1);
-
- tester.clock().advance(Duration.ofMillis(1000));
- tester.triggerJobs();
- app.runJob(productionUsWest1);
-
- // us-east-3 does not automatically trigger when paused, but does when forced.
- tester.triggerJobs();
- app.assertNotRunning(productionUsEast3);
- tester.deploymentTrigger().forceTrigger(app.instanceId(), productionUsEast3, "mrTrigger", true, true, false);
- app.assertRunning(productionUsEast3);
- assertFalse(app.instance().jobPause(productionUsEast3).isPresent());
- assertEquals(app.deployment(productionUsEast3.zone()).version(),
- tester.jobs().last(app.instanceId(), productionUsEast3).get().versions().targetPlatform());
- }
-
- @Test
- void applicationVersionIsNotDowngraded() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-central-1")
- .region("eu-west-1")
- .build();
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- // productionUsCentral1 fails after deployment, causing a mismatch between deployed and successful state.
- app.submit(applicationPackage)
- .runJob(systemTest)
- .runJob(stagingTest)
- .timeOutUpgrade(productionUsCentral1);
-
- RevisionId appVersion1 = app.lastSubmission().get();
- assertEquals(appVersion1, app.deployment(ZoneId.from("prod.us-central-1")).revision());
-
- // Verify the application change is not removed when platform change is cancelled.
- tester.deploymentTrigger().cancelChange(app.instanceId(), PLATFORM);
- assertEquals(Change.of(appVersion1), app.instance().change());
-
- // Now cancel the change as is done through the web API.
- tester.deploymentTrigger().cancelChange(app.instanceId(), ALL);
- assertEquals(Change.empty(), app.instance().change());
-
- // A new version is released, which should now deploy the currently deployed application version to avoid downgrades.
- Version version1 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- app.runJob(systemTest).runJob(stagingTest).failDeployment(productionUsCentral1);
-
- // The last job has a different target, and the tests need to run again.
- // These may now start, since the first job has been triggered once, and thus is verified already.
- app.runJob(systemTest).runJob(stagingTest);
-
- // Finally, the two production jobs complete, in order.
- app.runJob(productionUsCentral1).runJob(productionEuWest1);
- assertEquals(appVersion1, app.deployment(ZoneId.from("prod.us-central-1")).revision());
- }
-
- RevisionId latestDeployed(Instance instance) {
- return instance.productionDeployments().values().stream()
- .map(Deployment::revision)
- .reduce((o, n) -> require(o.equals(n), n, "all versions should be equal, but got " + o + " and " + n))
- .orElseThrow(() -> new AssertionError("no versions deployed"));
- }
-
- @Test
- void downgradingApplicationVersionWorks() {
- var app = tester.newDeploymentContext().submit().deploy();
- RevisionId appVersion0 = app.lastSubmission().get();
- assertEquals(appVersion0, latestDeployed(app.instance()));
-
- app.submit().deploy();
- RevisionId appVersion1 = app.lastSubmission().get();
- assertEquals(appVersion1, latestDeployed(app.instance()));
-
- // Downgrading application version.
- tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(appVersion0).withRevisionPin());
- assertEquals(Change.of(appVersion0).withRevisionPin(), app.instance().change());
- app.runJob(stagingTest)
- .runJob(productionUsCentral1)
- .runJob(productionUsEast3)
- .runJob(productionUsWest1);
- assertEquals(Change.empty().withRevisionPin(), app.instance().change());
- assertEquals(appVersion0, app.instance().deployments().get(productionUsEast3.zone()).revision());
- assertEquals(appVersion0, latestDeployed(app.instance()));
-
- tester.outstandingChangeDeployer().run();
- assertEquals(Change.empty().withRevisionPin(), app.instance().change());
- tester.deploymentTrigger().cancelChange(app.instanceId(), ALL);
- tester.outstandingChangeDeployer().run();
- assertEquals(Change.of(appVersion1), app.instance().change());
- }
-
- @Test
- void settingANoOpChangeIsANoOp() {
- var app = tester.newDeploymentContext().submit();
-
- app.deploy();
- RevisionId appVersion0 = app.lastSubmission().get();
- assertEquals(appVersion0, latestDeployed(app.instance()));
-
- app.submit().deploy();
- RevisionId appVersion1 = app.lastSubmission().get();
- assertEquals(appVersion1, latestDeployed(app.instance()));
-
- // Triggering a roll-out of an already deployed application is a no-op.
- assertEquals(Change.empty(), app.instance().change());
- tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(appVersion1));
- assertEquals(Change.empty(), app.instance().change());
- assertEquals(appVersion1, latestDeployed(app.instance()));
- }
-
- @Test
- void stepIsCompletePreciselyWhenItShouldBe() {
- var app1 = tester.newDeploymentContext("tenant1", "app1", "default");
- var app2 = tester.newDeploymentContext("tenant1", "app2", "default");
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-central-1")
- .region("eu-west-1")
- .build();
-
- // System upgrades to version0 and applications deploy on that version
- Version version0 = Version.fromString("7.0");
- tester.controllerTester().upgradeSystem(version0);
- app1.submit(applicationPackage).deploy();
- app2.submit(applicationPackage).deploy();
-
- // version1 is released and application1 skips upgrading to that version
- Version version1 = Version.fromString("7.1");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- // Deploy application2 to keep this version present in the system
- app2.deployPlatform(version1);
- tester.deploymentTrigger().cancelChange(app1.instanceId(), ALL);
-
- // version2 is released and application1 starts upgrading
- Version version2 = Version.fromString("7.2");
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().maintain();
- tester.triggerJobs();
- app1.jobAborted(systemTest).jobAborted(stagingTest);
- app1.runJob(systemTest).runJob(stagingTest).timeOutConvergence(productionUsCentral1);
- assertEquals(version2, app1.deployment(productionUsCentral1.zone()).version());
- Instant triggered = app1.instanceJobs().get(productionUsCentral1).lastTriggered().get().start();
- tester.clock().advance(Duration.ofHours(1));
-
- // version2 becomes broken and upgrade targets latest non-broken
- tester.upgrader().overrideConfidence(version2, VespaVersion.Confidence.broken);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain(); // Cancel upgrades to broken version
- assertEquals(Change.of(version1), app1.instance().change(), "Change becomes latest non-broken version");
-
- // version1 proceeds 'til the last job, where it fails; us-central-1 is skipped, as current change is strictly dominated by what's deployed there.
- app1.runJob(systemTest).runJob(stagingTest)
- .failDeployment(productionEuWest1);
- assertEquals(triggered, app1.instanceJobs().get(productionUsCentral1).lastTriggered().get().start());
-
- // Roll out a new application version, which gives a dual change -- this should trigger us-central-1, but only as long as it hasn't yet deployed there.
- RevisionId revision1 = app1.lastSubmission().get();
- app1.submit(applicationPackage);
- RevisionId revision2 = app1.lastSubmission().get();
- app1.runJob(systemTest) // Tests for new revision on version2
- .runJob(stagingTest)
- .runJob(systemTest) // Tests for new revision on version1
- .runJob(stagingTest);
- assertEquals(Change.of(version1).with(revision2), app1.instance().change());
- tester.triggerJobs();
- app1.assertRunning(productionUsCentral1);
- assertEquals(version2, app1.instance().deployments().get(productionUsCentral1.zone()).version());
- assertEquals(revision1, app1.deployment(productionUsCentral1.zone()).revision());
- assertTrue(triggered.isBefore(app1.instanceJobs().get(productionUsCentral1).lastTriggered().get().start()));
-
- // Change has a higher application version than what is deployed -- deployment should trigger.
- app1.timeOutUpgrade(productionUsCentral1);
- assertEquals(version2, app1.deployment(productionUsCentral1.zone()).version());
- assertEquals(revision2, app1.deployment(productionUsCentral1.zone()).revision());
-
- // Change is again strictly dominated, and us-central-1 is skipped, even though it is still failing.
- tester.clock().advance(Duration.ofHours(3)); // Enough time for retry
- tester.triggerJobs();
- // Failing job is not retried as change has been deployed
- app1.assertNotRunning(productionUsCentral1);
-
- // Last job has a different deployment target, so tests need to run again.
- app1.runJob(productionEuWest1) // Upgrade completes, and revision is the only change.
- .runJob(productionUsCentral1) // With only revision change, central should run to cover a previous failure.
- .runJob(productionEuWest1); // Finally, west changes revision.
- assertEquals(Change.empty(), app1.instance().change());
- assertEquals(Optional.of(RunStatus.success), app1.instanceJobs().get(productionUsCentral1).lastStatus());
- }
-
- @Test
- void eachParallelDeployTargetIsTested() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .parallel("eu-west-1", "us-east-3")
- .build();
- // Application version 1 and platform version 6.1.
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- // Success in first prod zone, change cancelled between triggering and completion of eu west job.
- // One of the parallel zones get a deployment, but both fail their jobs.
- Version v1 = new Version("6.1");
- Version v2 = new Version("6.2");
- tester.controllerTester().upgradeSystem(v2);
- tester.upgrader().maintain();
- app.runJob(systemTest).runJob(stagingTest);
- app.timeOutConvergence(productionEuWest1);
- tester.deploymentTrigger().cancelChange(app.instanceId(), PLATFORM);
- assertEquals(v2, app.deployment(productionEuWest1.zone()).version());
- assertEquals(v1, app.deployment(productionUsEast3.zone()).version());
-
- // New application version should run system and staging tests against both 6.1 and 6.2, in no particular order.
- app.submit(applicationPackage);
- tester.triggerJobs();
- Version firstTested = app.instanceJobs().get(systemTest).lastTriggered().get().versions().targetPlatform();
- assertEquals(firstTested, app.instanceJobs().get(stagingTest).lastTriggered().get().versions().targetPlatform());
-
- app.runJob(systemTest).runJob(stagingTest);
-
- // Test jobs for next production zone can start and run immediately.
- tester.triggerJobs();
- assertNotEquals(firstTested, app.instanceJobs().get(systemTest).lastTriggered().get().versions().targetPlatform());
- assertNotEquals(firstTested, app.instanceJobs().get(stagingTest).lastTriggered().get().versions().targetPlatform());
- app.runJob(systemTest).runJob(stagingTest);
-
- // Finish old run of the aborted production job.
- app.triggerJobs().jobAborted(productionUsEast3);
-
- // New upgrade is already tested for both jobs.
-
- // Both jobs fail again, and must be re-triggered -- this is ok, as they are both already triggered on their current targets.
- app.failDeployment(productionEuWest1).failDeployment(productionUsEast3)
- .runJob(productionEuWest1).runJob(productionUsEast3);
- assertFalse(app.instance().change().hasTargets());
- assertEquals(2, app.instanceJobs().get(productionEuWest1).lastSuccess().get().versions().targetRevision().number());
- assertEquals(2, app.instanceJobs().get(productionUsEast3).lastSuccess().get().versions().targetRevision().number());
- }
-
- @Test
- void retriesFailingJobs() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-central-1")
- .build();
-
- // Deploy completely on default application and platform versions
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- // New application change is deployed and fails in system-test for a while
- app.submit(applicationPackage).runJob(stagingTest).failDeployment(systemTest);
-
- // Retries immediately once
- app.failDeployment(systemTest);
- tester.triggerJobs();
- app.assertRunning(systemTest);
-
- // Stops immediate retry when next triggering is considered after first failure
- tester.clock().advance(Duration.ofSeconds(1));
- app.failDeployment(systemTest);
- tester.triggerJobs();
- app.assertNotRunning(systemTest);
-
- // Retries after 10 minutes since previous completion, plus half the time since the first failure
- tester.clock().advance(Duration.ofMinutes(10).plus(Duration.ofSeconds(1)));
- tester.triggerJobs();
- app.assertRunning(systemTest);
-
- // Retries less frequently as more time passes
- app.failDeployment(systemTest);
- tester.clock().advance(Duration.ofMinutes(15));
- tester.triggerJobs();
- app.assertNotRunning(systemTest);
-
- // Retries again when sufficient time has passed
- tester.clock().advance(Duration.ofSeconds(2));
- tester.triggerJobs();
- app.assertRunning(systemTest);
-
- // Still fails and is not retried
- app.failDeployment(systemTest);
- tester.triggerJobs();
- app.assertNotRunning(systemTest);
-
- // Another application change is deployed and fixes system-test. Change is triggered immediately as target changes
- app.submit(applicationPackage).deploy();
- assertTrue(tester.jobs().active().isEmpty(), "Deployment completed");
- }
-
- @Test
- void testPlatformVersionSelection() {
- // Setup system
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
- Version version1 = tester.controller().readSystemVersion();
- var app1 = tester.newDeploymentContext();
-
- // First deployment: An application change
- app1.submit(applicationPackage).deploy();
-
- assertEquals(version1, app1.application().oldestDeployedPlatform().get(), "First deployment gets system version");
- assertEquals(version1, tester.configServer().lastPrepareVersion().get());
-
- // Application change after a new system version, and a region added
- Version version2 = new Version(version1.getMajor(), version1.getMinor() + 1);
- tester.controllerTester().upgradeSystem(version2);
-
- applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .region("us-east-3")
- .build();
- app1.submit(applicationPackage).deploy();
- assertEquals(version1, app1.application().oldestDeployedPlatform().get(), "Application change preserves version, and new region gets oldest version too");
- assertEquals(version1, tester.configServer().lastPrepareVersion().get());
- assertFalse(app1.instance().change().hasTargets(), "Change deployed");
-
- tester.upgrader().maintain();
- app1.deployPlatform(version2);
-
- assertEquals(version2, app1.application().oldestDeployedPlatform().get(), "Version upgrade changes version");
- assertEquals(version2, tester.configServer().lastPrepareVersion().get());
- }
-
- @Test
- void requeueNodeAllocationFailureStagingJob() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-east-3")
- .build();
-
- var app1 = tester.newDeploymentContext("tenant1", "app1", "default").submit(applicationPackage);
- var app2 = tester.newDeploymentContext("tenant2", "app2", "default").submit(applicationPackage);
- var app3 = tester.newDeploymentContext("tenant3", "app3", "default").submit(applicationPackage);
-
- // all applications: system-test completes successfully with some time in between, to determine trigger order.
- app2.runJob(systemTest);
- tester.clock().advance(Duration.ofMinutes(1));
-
- app1.runJob(systemTest);
- tester.clock().advance(Duration.ofMinutes(1));
-
- app3.runJob(systemTest);
-
- // all applications: staging test jobs queued
- tester.triggerJobs();
- assertEquals(3, tester.jobs().active().size());
-
- // Abort all running jobs, so we have three candidate jobs, of which only one should be triggered at a time.
- tester.abortAll();
-
- assertEquals(List.of(), tester.jobs().active());
-
- tester.readyJobsTrigger().maintain();
- assertEquals(1, tester.jobs().active().size());
-
- tester.readyJobsTrigger().maintain();
- assertEquals(2, tester.jobs().active().size());
-
- tester.readyJobsTrigger().maintain();
- assertEquals(3, tester.jobs().active().size());
-
- // Remove the jobs for app1 and app2, and then let app3 fail node allocation.
- // All three jobs are now eligible, but the one for app3 should trigger first as a nodeAllocationFailure-retry.
- app3.nodeAllocationFailure(stagingTest);
- app1.abortJob(stagingTest);
- app2.abortJob(stagingTest);
-
- tester.readyJobsTrigger().maintain();
- app3.assertRunning(stagingTest);
- assertEquals(1, tester.jobs().active().size());
-
- tester.readyJobsTrigger().maintain();
- assertEquals(2, tester.jobs().active().size());
-
- tester.readyJobsTrigger().maintain();
- assertEquals(3, tester.jobs().active().size());
-
- // Finish deployment for apps 2 and 3, then release a new version, leaving only app1 with an application upgrade.
- app2.deploy();
- app3.deploy();
- app1.runJob(stagingTest);
- assertEquals(0, tester.jobs().active().size());
-
- tester.controllerTester().upgradeSystem(new Version("6.2"));
- tester.upgrader().maintain();
- app1.submit(applicationPackage);
-
- // Tests for app1 trigger before the others since it carries an application upgrade.
- tester.readyJobsTrigger().run();
- app1.assertRunning(systemTest);
- app1.assertRunning(stagingTest);
- assertEquals(2, tester.jobs().active().size());
-
- // Let the test jobs start, remove everything except system test for app3, which fails node allocation again.
- tester.triggerJobs();
- app3.nodeAllocationFailure(systemTest);
- app1.abortJob(systemTest);
- app1.abortJob(stagingTest);
- app2.abortJob(systemTest);
- app2.abortJob(stagingTest);
- app3.abortJob(stagingTest);
- assertEquals(0, tester.jobs().active().size());
-
- assertTrue(app1.instance().change().revision().isPresent());
- assertFalse(app2.instance().change().revision().isPresent());
- assertFalse(app3.instance().change().revision().isPresent());
-
- tester.readyJobsTrigger().maintain();
- app1.assertRunning(stagingTest);
- app3.assertRunning(systemTest);
- assertEquals(2, tester.jobs().active().size());
-
- tester.readyJobsTrigger().maintain();
- app1.assertRunning(systemTest);
- assertEquals(4, tester.jobs().active().size());
-
- tester.readyJobsTrigger().maintain();
- app3.assertRunning(stagingTest);
- app2.assertRunning(stagingTest);
- app2.assertRunning(systemTest);
- assertEquals(6, tester.jobs().active().size());
- }
-
- @Test
- void testUserInstancesNotInDeploymentSpec() {
- var app = tester.newDeploymentContext();
- tester.controller().applications().createInstance(app.application().id().instance("user"));
- app.submit().deploy();
- }
-
- @Test
- void testMultipleInstancesWithDifferentChanges() {
- DeploymentContext i1 = tester.newDeploymentContext("t", "a", "i1");
- DeploymentContext i2 = tester.newDeploymentContext("t", "a", "i2");
- DeploymentContext i3 = tester.newDeploymentContext("t", "a", "i3");
- DeploymentContext i4 = tester.newDeploymentContext("t", "a", "i4");
- ApplicationPackage applicationPackage = ApplicationPackageBuilder
- .fromDeploymentXml("""
- <deployment version='1'>
- <upgrade revision-change='when-failing' />
- <parallel>
- <instance id='i1'>
- <prod>
- <region>us-east-3</region>
- <delay hours='6' />
- </prod>
- </instance>
- <instance id='i2'>
- <prod>
- <region>us-east-3</region>
- </prod>
- </instance>
- </parallel>
- <instance id='i3'>
- <prod>
- <region>us-east-3</region>
- <delay hours='18' />
- <test>us-east-3</test>
- </prod>
- </instance>
- <instance id='i4'>
- <test />
- <staging />
- <prod>
- <region>us-east-3</region>
- </prod>
- </instance>
- </deployment>
- """);
-
- // Package is submitted, and change propagated to the two first instances.
- i1.submit(applicationPackage);
- Optional<RevisionId> v0 = i1.lastSubmission();
- tester.outstandingChangeDeployer().run();
- assertEquals(v0, i1.instance().change().revision());
- assertEquals(v0, i2.instance().change().revision());
- assertEquals(Optional.empty(), i3.instance().change().revision());
- assertEquals(Optional.empty(), i4.instance().change().revision());
-
- // Tests run in i4, as they're declared there, and i1 and i2 get to work
- i4.runJob(systemTest).runJob(stagingTest);
- i1.runJob(productionUsEast3);
- i2.runJob(productionUsEast3);
-
- // Since the post-deployment delay of i1 is incomplete, i3 doesn't yet get the change.
- tester.outstandingChangeDeployer().run();
- assertEquals(v0, Optional.of(latestDeployed(i1.instance())));
- assertEquals(v0, Optional.of(latestDeployed(i2.instance())));
- assertEquals(Optional.empty(), i1.instance().change().revision());
- assertEquals(Optional.empty(), i2.instance().change().revision());
- assertEquals(Optional.empty(), i3.instance().change().revision());
- assertEquals(Optional.empty(), i4.instance().change().revision());
-
- // When the delay is done, i3 gets the change.
- tester.clock().advance(Duration.ofHours(6));
- tester.outstandingChangeDeployer().run();
- assertEquals(Optional.empty(), i1.instance().change().revision());
- assertEquals(Optional.empty(), i2.instance().change().revision());
- assertEquals(v0, i3.instance().change().revision());
- assertEquals(Optional.empty(), i4.instance().change().revision());
-
- // v0 begins roll-out in i3, and v1 is submitted and rolls out in i1 and i2 some time later
- i3.runJob(productionUsEast3); // v0
- tester.clock().advance(Duration.ofHours(12));
- i1.submit(applicationPackage);
- Optional<RevisionId> v1 = i1.lastSubmission();
- i4.runJob(systemTest).runJob(stagingTest);
- i1.runJob(productionUsEast3); // v1
- i2.runJob(productionUsEast3); // v1
- assertEquals(v1, Optional.of(latestDeployed(i1.instance())));
- assertEquals(v1, Optional.of(latestDeployed(i2.instance())));
- assertEquals(Optional.empty(), i1.instance().change().revision());
- assertEquals(Optional.empty(), i2.instance().change().revision());
- assertEquals(v0, i3.instance().change().revision());
- assertEquals(Optional.empty(), i4.instance().change().revision());
-
- // After some time, v2 also starts rolling out to i1 and i2, but does not complete in i2
- tester.clock().advance(Duration.ofHours(3));
- i1.submit(applicationPackage);
- Optional<RevisionId> v2 = i1.lastSubmission();
- i4.runJob(systemTest).runJob(stagingTest);
- i1.runJob(productionUsEast3); // v2
- tester.clock().advance(Duration.ofHours(3));
-
- // v1 is all done in i1 and i2, but does not yet roll out in i3; v2 is not completely rolled out there yet.
- tester.outstandingChangeDeployer().run();
- assertEquals(v0, i3.instance().change().revision());
-
- // i3 completes v0, which rolls out to i4; v1 is ready for i3, but v2 is not.
- i3.runJob(testUsEast3);
- assertEquals(Optional.empty(), i3.instance().change().revision());
- tester.outstandingChangeDeployer().run();
- assertEquals(v2, Optional.of(latestDeployed(i1.instance())));
- assertEquals(v1, Optional.of(latestDeployed(i2.instance())));
- assertEquals(v0, Optional.of(latestDeployed(i3.instance())));
- assertEquals(Optional.empty(), i1.instance().change().revision());
- assertEquals(v2, i2.instance().change().revision());
- assertEquals(v1, i3.instance().change().revision());
- assertEquals(v0, i4.instance().change().revision());
- }
-
- @Test
- void testMultipleInstancesWithRevisionCatchingUpToUpgrade() {
- String spec = """
- <deployment>
- <instance id='alpha'>
- <upgrade rollout="simultaneous" revision-target="next" />
- <test />
- <staging />
- </instance>
- <instance id='beta'>
- <upgrade rollout="simultaneous" revision-change="when-clear" revision-target="next" />
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </prod>
- </instance>
- </deployment>
- """;
- ApplicationPackage applicationPackage = ApplicationPackageBuilder.fromDeploymentXml(spec);
- DeploymentContext alpha = tester.newDeploymentContext("t", "a", "alpha");
- DeploymentContext beta = tester.newDeploymentContext("t", "a", "beta");
- alpha.submit(applicationPackage).deploy();
-
- Version version1 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().run();
- alpha.runJob(systemTest).runJob(stagingTest);
- assertEquals(Change.empty(), alpha.instance().change());
- assertEquals(Change.empty(), beta.instance().change());
-
- tester.upgrader().run();
- assertEquals(Change.empty(), alpha.instance().change());
- assertEquals(Change.of(version1), beta.instance().change());
-
- tester.outstandingChangeDeployer().run();
- beta.triggerJobs();
- tester.runner().run();
- tester.outstandingChangeDeployer().run();
- beta.triggerJobs();
- tester.outstandingChangeDeployer().run();
- beta.assertRunning(productionUsEast3);
- beta.assertNotRunning(testUsEast3);
-
- alpha.submit(applicationPackage);
- Optional<RevisionId> revision2 = alpha.lastSubmission();
- assertEquals(Change.of(revision2.get()), alpha.instance().change());
- assertEquals(Change.of(version1), beta.instance().change());
-
- alpha.runJob(systemTest).runJob(stagingTest);
- assertEquals(Change.empty(), alpha.instance().change());
- assertEquals(Change.of(version1), beta.instance().change());
-
- tester.outstandingChangeDeployer().run();
- assertEquals(Change.of(version1).with(revision2.get()), beta.instance().change());
-
- beta.triggerJobs();
- tester.runner().run();
- beta.triggerJobs();
-
- beta.assertRunning(productionUsEast3);
- beta.assertNotRunning(testUsEast3);
-
- beta.runJob(productionUsEast3)
- .runJob(testUsEast3);
-
- assertEquals(Change.empty(), beta.instance().change());
- }
-
- @Test
- void testMultipleInstances() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .instances("instance1,instance2")
- .region("us-east-3")
- .build();
- var app = tester.newDeploymentContext("tenant1", "application1", "instance1")
- .submit(applicationPackage)
- .completeRollout();
- assertEquals(2, app.application().instances().size());
- assertEquals(2, app.application().productionDeployments().values().stream()
- .mapToInt(Collection::size)
- .sum());
- }
-
- @Test
- void testDeclaredProductionTests() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-east-3")
- .delay(Duration.ofMinutes(1))
- .test("us-east-3")
- .region("us-west-1")
- .region("us-central-1")
- .test("us-central-1")
- .test("us-west-1")
- .region("eu-west-1")
- .build();
- var app = tester.newDeploymentContext().submit(applicationPackage);
-
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsEast3);
- app.assertNotRunning(productionUsWest1);
-
- tester.clock().advance(Duration.ofMinutes(1));
- app.runJob(testUsEast3)
- .runJob(productionUsWest1).runJob(productionUsCentral1)
- .runJob(testUsCentral1).runJob(testUsWest1)
- .runJob(productionEuWest1);
- assertEquals(Change.empty(), app.instance().change());
-
- // Application starts upgrade, but confidence is broken after first zone. Tests won't run.
- Version version0 = app.application().oldestDeployedPlatform().get();
- Version version1 = Version.fromString("6.7");
- Version version2 = Version.fromString("6.8");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- tester.newDeploymentContext("keep", "version1", "alive").submit().deploy();
-
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsEast3);
- tester.clock().advance(Duration.ofMinutes(1));
- app.failDeployment(testUsEast3);
- tester.triggerJobs();
- app.assertRunning(testUsEast3);
-
- tester.upgrader().overrideConfidence(version1, VespaVersion.Confidence.broken);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- app.failDeployment(testUsEast3);
- app.assertNotRunning(testUsEast3);
- assertEquals(Change.empty(), app.instance().change());
-
- // Application is pinned to previous version, and downgrades to that. Tests are re-run.
- tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(version0).withPlatformPin());
- app.runJob(stagingTest).runJob(productionUsEast3);
- tester.clock().advance(Duration.ofMinutes(1));
- app.failDeployment(testUsEast3);
- tester.clock().advance(Duration.ofMinutes(11)); // Job is cooling down after consecutive failures.
- app.runJob(testUsEast3);
- assertEquals(Change.empty().withPlatformPin(), app.instance().change());
-
- // A new upgrade is attempted, and production tests wait for redeployment.
- tester.controllerTester().upgradeSystem(version2);
- tester.deploymentTrigger().cancelChange(app.instanceId(), ALL);
-
- tester.upgrader().overrideConfidence(version1, VespaVersion.Confidence.high);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain(); // App should target version2.
- assertEquals(Change.of(version2), app.instance().change());
-
- // App partially upgrades to version2.
- app.runJob(systemTest).runJob(stagingTest);
- app.triggerJobs();
- app.assertRunning(productionUsEast3);
- app.assertNotRunning(testUsEast3);
- app.runJob(productionUsEast3);
- tester.clock().advance(Duration.ofMinutes(1));
- app.runJob(testUsEast3).runJob(productionUsWest1).triggerJobs();
- app.assertRunning(productionUsCentral1);
- tester.runner().run();
- app.triggerJobs();
- app.assertNotRunning(testUsCentral1);
- app.assertNotRunning(testUsWest1);
-
- // Version2 gets broken, but Version1 has high confidence now, and is the new target.
- // Since us-east-3 is already on Version2, both deployment and tests to it should be skipped.
- tester.upgrader().overrideConfidence(version2, VespaVersion.Confidence.broken);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain(); // App should target version2.
- assertEquals(Change.of(version1), app.instance().change());
- app.triggerJobs();
-
- // Deployment to 6.8 already happened, so a downgrade to 6.7 won't, but production tests will still run.
- app.timeOutConvergence(productionUsCentral1);
- app.runJob(testUsCentral1).runJob(testUsWest1).runJob(productionEuWest1);
- assertEquals(version1, app.instance().deployments().get(ZoneId.from("prod.eu-west-1")).version());
- }
-
- @Test
- void testDeployComplicatedDeploymentSpec() {
- String complicatedDeploymentSpec =
- """
- <deployment version='1.0' athenz-domain='domain' athenz-service='service'>
- <instance id='dev'>
- <dev />
- </instance>
- <parallel>
- <instance id='instance' athenz-service='in-service'>
- <staging />
- <prod>
- <parallel>
- <region>us-west-1</region>
- <steps>
- <region>us-east-3</region>
- <delay hours='2' />
- <region>eu-west-1</region>
- <delay hours='2' />
- </steps>
- <steps>
- <delay hours='3' />
- <region>us-central-1</region>
- <parallel>
- <region athenz-service='no-service'>ap-northeast-1</region>
- <region>ap-northeast-2</region>
- <test>us-central-1</test>
- </parallel>
- </steps>
- <delay hours='3' minutes='30' />
- </parallel>
- <parallel>
- <test>ap-northeast-2</test>
- <test>ap-northeast-1</test>
- </parallel>
- <test>us-east-3</test>
- <region>ap-southeast-1</region>
- </prod>
- <endpoints>
- <endpoint id='foo' container-id='bar'>
- <region>us-east-3</region>
- </endpoint>
- <endpoint id='nalle' container-id='frosk' />
- <endpoint container-id='quux' />
- </endpoints>
- </instance>
- <instance id='other'>
- <upgrade policy='conservative' />
- <test />
- <block-change revision='true' version='false' days='sat' hours='0-23' time-zone='CET' />
- <prod>
- <region>eu-west-1</region>
- <test>eu-west-1</test>
- </prod>
- <notifications when='failing'>
- <email role='author' />
- <email address='john@dev' when='failing-commit' />
- <email address='jane@dev' />
- </notifications>
- </instance>
- </parallel>
- <instance id='last'>
- <upgrade policy='conservative' />
- <prod>
- <region>eu-west-1</region>
- </prod>
- </instance>
- </deployment>
- """;
-
- tester.atMondayMorning();
- ApplicationPackage applicationPackage = ApplicationPackageBuilder.fromDeploymentXml(complicatedDeploymentSpec);
- var app1 = tester.newDeploymentContext("t", "a", "instance").submit(applicationPackage);
- var app2 = tester.newDeploymentContext("t", "a", "other");
- var app3 = tester.newDeploymentContext("t", "a", "last");
-
- // Verify that the first submission rolls out as per the spec.
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size());
- app1.runJob(stagingTest);
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size());
- app2.runJob(systemTest);
-
- app1.runJob(productionUsWest1);
- tester.triggerJobs();
- assertEquals(3, tester.jobs().active().size());
- app1.runJob(productionUsEast3);
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size());
-
- tester.clock().advance(Duration.ofHours(2));
-
- app1.runJob(productionEuWest1);
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size());
- app2.assertNotRunning(testEuWest1);
- app2.runJob(productionEuWest1);
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size());
- app2.runJob(testEuWest1);
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size());
-
- tester.clock().advance(Duration.ofHours(1));
- app1.runJob(productionUsCentral1);
- tester.triggerJobs();
- assertEquals(4, tester.jobs().active().size());
- app1.runJob(testUsCentral1);
- tester.triggerJobs();
- assertEquals(3, tester.jobs().active().size());
- app1.runJob(productionApNortheast2);
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size());
- app1.runJob(productionApNortheast1);
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size());
-
- tester.clock().advance(Duration.ofMinutes(30));
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size());
-
- tester.clock().advance(Duration.ofMinutes(30));
- app1.runJob(testApNortheast1);
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size());
- app1.runJob(testApNortheast2);
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size());
- app1.runJob(testUsEast3);
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size());
- app1.runJob(productionApSoutheast1);
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size());
- app3.runJob(productionEuWest1);
- tester.triggerJobs();
- assertEquals(List.of(), tester.jobs().active());
-
- tester.atMondayMorning().clock().advance(Duration.ofDays(5)); // Inside revision block window for second, conservative instance.
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
- tester.upgrader().maintain();
- assertEquals(Change.of(version), app1.instance().change());
- assertEquals(Change.empty(), app2.instance().change());
- assertEquals(Change.empty(), app3.instance().change());
-
- // Upgrade instance 1; upgrade rolls out first, with revision following.
- // The new platform won't roll out to the conservative instance until the normal one is upgraded.
- app1.submit(applicationPackage);
- assertEquals(Change.of(version).with(app1.application().revisions().last().get().id()), app1.instance().change());
- // Upgrade platform.
- app2.runJob(systemTest);
- app1.runJob(stagingTest)
- .runJob(productionUsWest1)
- .runJob(productionUsEast3);
- // Upgrade revision
- tester.clock().advance(Duration.ofSeconds(1)); // Ensure we see revision as rolling after upgrade.
- app2.runJob(systemTest); // R
- app1.runJob(stagingTest) // R
- .runJob(productionUsWest1); // R
- // productionUsEast3 won't change revision before its production test has completed for the upgrade, which is one of the last jobs!
- tester.clock().advance(Duration.ofHours(2));
- app1.runJob(productionEuWest1);
- tester.clock().advance(Duration.ofHours(1));
- app1.runJob(productionUsCentral1);
- app1.runJob(testUsCentral1);
- tester.clock().advance(Duration.ofSeconds(1));
- app1.runJob(productionUsCentral1); // R
- app1.runJob(testUsCentral1); // R
- app1.runJob(productionApNortheast2);
- app1.runJob(productionApNortheast1);
- tester.clock().advance(Duration.ofHours(1));
- app1.runJob(testApNortheast1);
- app1.runJob(testApNortheast2);
- app1.runJob(productionApNortheast2); // R
- app1.runJob(productionApNortheast1); // R
- app1.runJob(testUsEast3);
- app1.runJob(productionApSoutheast1);
- tester.clock().advance(Duration.ofSeconds(1));
- app1.runJob(productionUsEast3); // R
- tester.clock().advance(Duration.ofHours(2));
- app1.runJob(productionEuWest1); // R
- tester.clock().advance(Duration.ofMinutes(330));
- app1.runJob(testApNortheast1); // R
- app1.runJob(testApNortheast2); // R
- app1.runJob(testUsEast3); // R
- app1.runJob(productionApSoutheast1); // R
-
- app1.runJob(stagingTest); // Tests with only the outstanding application change.
- app2.runJob(systemTest); // Tests with only the outstanding application change.
-
- // Confidence rises to 'high', for the new version, and instance 2 starts to upgrade.
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- tester.outstandingChangeDeployer().run();
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size(), tester.jobs().active().toString());
- assertEquals(Change.empty(), app1.instance().change());
- assertEquals(Change.of(version), app2.instance().change());
- assertEquals(Change.empty(), app3.instance().change());
-
- app2.runJob(productionEuWest1)
- .failDeployment(testEuWest1);
-
- // Instance 2 failed the last job, and now exits block window, letting application change roll out with the upgrade.
- tester.clock().advance(Duration.ofDays(1)); // Leave block window for revisions.
- tester.upgrader().maintain();
- tester.outstandingChangeDeployer().run();
- assertEquals(0, tester.jobs().active().size());
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size());
- assertEquals(Change.empty(), app1.instance().change());
- assertEquals(Change.of(version).with(app1.application().revisions().last().get().id()), app2.instance().change());
-
- app2.runJob(productionEuWest1)
- .runJob(testEuWest1);
- assertEquals(Change.empty(), app2.instance().change());
- assertEquals(Change.empty(), app3.instance().change());
-
- // Two first instances upgraded and with new revision — last instance gets both changes as well.
- tester.upgrader().maintain();
- tester.outstandingChangeDeployer().run();
- assertEquals(Change.of(version).with(app1.lastSubmission().get()), app3.instance().change());
-
- tester.deploymentTrigger().cancelChange(app3.instanceId(), ALL);
- tester.outstandingChangeDeployer().run();
- tester.upgrader().maintain();
- assertEquals(Change.of(app1.lastSubmission().get()), app3.instance().change());
-
- app3.runJob(productionEuWest1);
- tester.upgrader().maintain();
- app1.runJob(stagingTest);
- app3.runJob(productionEuWest1);
- tester.triggerJobs();
- assertEquals(List.of(), tester.jobs().active());
- assertEquals(Change.empty(), app3.instance().change());
- }
-
- @Test
- void testRevisionJoinsUpgradeWithSeparateRollout() {
- var appPackage = new ApplicationPackageBuilder().region("us-central-1")
- .region("us-east-3")
- .region("us-west-1")
- .upgradeRollout("separate")
- .build();
- var app = tester.newDeploymentContext().submit(appPackage).deploy();
-
- // Platform rolls through first production zone.
- var version0 = tester.controller().readSystemVersion();
- var version1 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1);
- tester.clock().advance(Duration.ofMinutes(1));
-
- // Revision starts rolling, but stays behind.
- var revision0 = app.lastSubmission();
- app.submit(appPackage);
- var revision1 = app.lastSubmission();
- assertEquals(Change.of(version1).with(revision1.get()), app.instance().change());
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1);
-
- // Upgrade got here first, so attempts to proceed alone, but the upgrade fails.
- app.triggerJobs();
- assertEquals(new Versions(version1, revision0.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsEast3).get().versions());
- app.timeOutConvergence(productionUsEast3);
-
- // Revision is allowed to join.
- app.triggerJobs();
- assertEquals(new Versions(version1, revision1.get(), Optional.of(version1), revision0),
- tester.jobs().last(app.instanceId(), productionUsEast3).get().versions());
- app.runJob(productionUsEast3);
-
- // Platform and revision now proceed together.
- app.runJob(stagingTest);
- app.triggerJobs();
- assertEquals(new Versions(version1, revision1.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsWest1).get().versions());
- app.runJob(productionUsWest1);
- assertEquals(Change.empty(), app.instance().change());
-
- // New upgrade fails in staging-test, and revision to fix it is submitted.
- var version2 = new Version("6.3");
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().maintain();
- app.runJob(systemTest).failDeployment(stagingTest);
- tester.clock().advance(Duration.ofMinutes(30));
- app.failDeployment(stagingTest);
- app.submit(appPackage);
-
- app.runJob(systemTest).runJob(stagingTest) // Tests run with combined upgrade.
- .runJob(productionUsCentral1) // Combined upgrade stays together.
- .runJob(productionUsEast3).runJob(productionUsWest1);
- assertEquals(Map.of(), app.deploymentStatus().jobsToRun());
- assertEquals(Change.empty(), app.instance().change());
- }
-
- @Test
- void testProductionTestBlockingDeploymentWithSeparateRollout() {
- var appPackage = new ApplicationPackageBuilder().region("us-east-3")
- .region("us-west-1")
- .delay(Duration.ofHours(1))
- .test("us-east-3")
- .upgradeRollout("separate")
- .build();
- var app = tester.newDeploymentContext().submit(appPackage)
- .runJob(systemTest).runJob(stagingTest)
- .runJob(productionUsEast3).runJob(productionUsWest1);
- tester.clock().advance(Duration.ofHours(1));
- app.runJob(testUsEast3);
- assertEquals(Change.empty(), app.instance().change());
-
- // Platform rolls through first production zone.
- var version0 = tester.controller().readSystemVersion();
- var version1 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsEast3);
-
- // Revision starts rolling, but waits for production test to verify the upgrade.
- var revision0 = app.lastSubmission();
- app.submit(appPackage);
- var revision1 = app.lastSubmission();
- assertEquals(Change.of(version1).with(revision1.get()), app.instance().change());
- app.runJob(systemTest).runJob(stagingTest).triggerJobs();
- app.assertRunning(productionUsWest1);
- app.assertNotRunning(productionUsEast3);
-
- // Upgrade got here first, so attempts to proceed alone, but the upgrade fails.
- app.triggerJobs();
- assertEquals(new Versions(version1, revision0.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsWest1).get().versions());
- app.timeOutConvergence(productionUsWest1).triggerJobs();
-
- // Upgrade now fails between us-east-3 deployment and test, so test is abandoned, and revision unblocked.
- app.assertRunning(productionUsEast3);
- assertEquals(new Versions(version1, revision1.get(), Optional.of(version1), revision0),
- tester.jobs().last(app.instanceId(), productionUsEast3).get().versions());
- app.runJob(productionUsEast3).triggerJobs()
- .jobAborted(productionUsWest1).runJob(productionUsWest1);
- tester.clock().advance(Duration.ofHours(1));
- app.runJob(testUsEast3);
- assertEquals(Change.empty(), app.instance().change());
- }
-
- @Test
- void testProductionTestNotBlockingDeploymentWithSimultaneousRollout() {
- var appPackage = new ApplicationPackageBuilder().region("us-east-3")
- .region("us-central-1")
- .region("us-west-1")
- .delay(Duration.ofHours(1))
- .test("us-east-3")
- .test("us-west-1")
- .upgradeRollout("simultaneous")
- .build();
- var app = tester.newDeploymentContext().submit(appPackage)
- .runJob(systemTest).runJob(stagingTest)
- .runJob(productionUsEast3).runJob(productionUsCentral1).runJob(productionUsWest1);
- tester.clock().advance(Duration.ofHours(1));
- app.runJob(testUsEast3).runJob(testUsWest1);
- assertEquals(Change.empty(), app.instance().change());
-
- // Platform rolls through first production zone.
- var version0 = tester.controller().readSystemVersion();
- var version1 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsEast3);
-
- // Revision starts rolling, and causes production test to abort when it reaches deployment.
- var revision0 = app.lastSubmission();
- app.submit(appPackage);
- var revision1 = app.lastSubmission();
- assertEquals(Change.of(version1).with(revision1.get()), app.instance().change());
- app.runJob(systemTest).runJob(stagingTest).triggerJobs();
- app.assertRunning(productionUsCentral1);
- app.assertRunning(productionUsEast3);
-
- // Revision deploys to first prod zone.
- app.triggerJobs();
- assertEquals(new Versions(version1, revision1.get(), Optional.of(version1), revision0),
- tester.jobs().last(app.instanceId(), productionUsEast3).get().versions());
- tester.clock().advance(Duration.ofSeconds(1));
- app.runJob(productionUsEast3);
-
- // Revision catches up in second prod zone.
- app.runJob(systemTest).runJob(stagingTest).runJob(stagingTest).triggerJobs();
- app.jobAborted(productionUsCentral1).triggerJobs();
- assertEquals(new Versions(version1, revision1.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsCentral1).get().versions());
- app.runJob(productionUsCentral1).triggerJobs();
-
- // Revision proceeds alone in third prod zone, making test targets different for the two prod tests.
- assertEquals(new Versions(version0, revision1.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsWest1).get().versions());
- app.runJob(productionUsWest1);
- app.triggerJobs();
- app.assertNotRunning(testUsEast3);
- tester.clock().advance(Duration.ofHours(1));
-
- // Test lets revision proceed alone, and us-west-1 is blocked until tested.
- app.runJob(testUsEast3).triggerJobs();
- app.assertNotRunning(productionUsWest1);
- app.runJob(testUsWest1).runJob(productionUsWest1).runJob(testUsWest1); // Test for us-east-3 is not re-run.
- assertEquals(Change.empty(), app.instance().change());
- }
-
- @Test
- void testVeryLengthyPipelineRevisions() {
- String lengthyDeploymentSpec =
- """
- <deployment version='1.0'>
- <instance id='alpha'>
- <test />
- <staging />
- <upgrade revision-change='always' />
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </prod>
- </instance>
- <instance id='beta'>
- <upgrade revision-change='when-failing' />
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </prod>
- </instance>
- <instance id='gamma'>
- <upgrade revision-change='when-clear' revision-target='next' min-risk='3' max-risk='6' />
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </prod>
- </instance>
- </deployment>
- """;
- var appPackage = ApplicationPackageBuilder.fromDeploymentXml(lengthyDeploymentSpec);
- var alpha = tester.newDeploymentContext("t", "a", "alpha");
- var beta = tester.newDeploymentContext("t", "a", "beta");
- var gamma = tester.newDeploymentContext("t", "a", "gamma");
- alpha.submit(appPackage, 0).deploy();
-
- // revision2 is submitted, and rolls through alpha.
- var revision1 = alpha.lastSubmission();
- alpha.submit(appPackage, 3); // Risk high enough that this may roll out alone to gamma.
- var revision2 = alpha.lastSubmission();
-
- alpha.runJob(systemTest).runJob(stagingTest)
- .runJob(productionUsEast3).runJob(testUsEast3);
- assertEquals(Optional.empty(), alpha.instance().change().revision());
-
- // revision3 is submitted when revision2 is half-way.
- tester.outstandingChangeDeployer().run();
- beta.runJob(productionUsEast3);
- alpha.submit(appPackage, 2); // Will only roll out to gamma together with the next revision.
- var revision3 = alpha.lastSubmission();
- beta.runJob(testUsEast3);
- assertEquals(Optional.empty(), beta.instance().change().revision());
-
- // revision3 is the target for alpha, beta is done, revision2 is the target for gamma.
- tester.outstandingChangeDeployer().run();
- assertEquals(revision3, alpha.instance().change().revision());
- assertEquals(Optional.empty(), beta.instance().change().revision());
- assertEquals(revision2, gamma.instance().change().revision());
-
- // revision3 rolls to beta, then a couple of new revisions are submitted to alpha, and the latter is the new target.
- alpha.runJob(systemTest).runJob(stagingTest)
- .runJob(productionUsEast3).runJob(testUsEast3);
- tester.outstandingChangeDeployer().run();
- assertEquals(Optional.empty(), alpha.instance().change().revision());
- assertEquals(revision3, beta.instance().change().revision());
-
- // revision5 supersedes revision4
- alpha.submit(appPackage, 3);
- var revision4 = alpha.lastSubmission();
- alpha.runJob(systemTest).runJob(stagingTest)
- .runJob(productionUsEast3);
- alpha.submit(appPackage, 2);
- var revision5 = alpha.lastSubmission();
- alpha.runJob(systemTest).runJob(stagingTest)
- .runJob(productionUsEast3).runJob(testUsEast3);
- tester.outstandingChangeDeployer().run();
- assertEquals(Optional.empty(), alpha.instance().change().revision());
- assertEquals(revision3, beta.instance().change().revision());
-
- // revision6 rolls through alpha, and becomes the next target for beta, which also completes revision3.
- alpha.submit(appPackage, 6);
- var revision6 = alpha.lastSubmission();
- alpha.runJob(systemTest).runJob(stagingTest)
- .runJob(productionUsEast3)
- .runJob(testUsEast3);
- beta.runJob(productionUsEast3).runJob(testUsEast3);
- tester.outstandingChangeDeployer().run();
- assertEquals(Optional.empty(), alpha.instance().change().revision());
- assertEquals(revision6, beta.instance().change().revision());
-
- // revision 2 fails in gamma, but this does not bring on revision 3
- gamma.failDeployment(productionUsEast3);
- tester.outstandingChangeDeployer().run();
- assertEquals(revision2, gamma.instance().change().revision());
-
- // revision 2 completes in gamma
- gamma.runJob(productionUsEast3)
- .runJob(testUsEast3);
- tester.outstandingChangeDeployer().run();
- assertEquals(Optional.empty(), alpha.instance().change().revision());
- assertEquals(Optional.empty(), gamma.instance().change().revision()); // no other revisions after 3 are ready, so gamma waits
-
- // revision6 rolls through beta, and revision3 is the next target for gamma with "when-clear" change-revision, now that 6 is blocking 4 and 5
- alpha.jobAborted(stagingTest).runJob(stagingTest);
- beta.runJob(productionUsEast3).runJob(testUsEast3);
- assertEquals(Optional.empty(), beta.instance().change().revision());
-
- tester.outstandingChangeDeployer().run();
- assertEquals(Optional.empty(), alpha.instance().change().revision());
- assertEquals(Optional.empty(), beta.instance().change().revision());
- assertEquals(revision3, gamma.instance().change().revision()); // revision4 never became ready, but 5 did, so 4 is skipped, and 3 rolls out alone instead.
-
- // revision 6 is next, once 3 is done
- // revision 3 completes
- gamma.runJob(productionUsEast3)
- .runJob(testUsEast3);
- tester.outstandingChangeDeployer().run();
- assertEquals(revision6, gamma.instance().change().revision());
-
- // revision 7 becomes ready for gamma, but must wait for the idle time of 8 hours before being deployed
- alpha.submit(appPackage, 1);
- var revision7 = alpha.lastSubmission();
- alpha.deploy();
- tester.outstandingChangeDeployer();
- assertEquals(Change.empty(), gamma.instance().change());
- assertEquals(revision6.get(), gamma.deployment(ZoneId.from("prod.us-east-3")).revision());
-
- tester.clock().advance(Duration.ofHours(8));
- tester.outstandingChangeDeployer().run();
- assertEquals(revision7, gamma.instance().change().revision());
-
- // revision 8 is has too low risk to roll out on its own, but will start rolling immediately when revision 9 is submitted
- gamma.deploy();
- alpha.submit(appPackage, 2);
- var revision8 = alpha.lastSubmission();
- alpha.deploy();
- tester.outstandingChangeDeployer();
- assertEquals(Change.empty(), gamma.instance().change());
- assertEquals(revision7.get(), gamma.deployment(ZoneId.from("prod.us-east-3")).revision());
-
- alpha.submit(appPackage, 5);
- tester.outstandingChangeDeployer().run();
- assertEquals(revision8, gamma.instance().change().revision());
- }
-
- @Test
- void testVeryLengthyPipelineUpgrade() {
- String lengthyDeploymentSpec =
- """
- <deployment version='1.0'>
- <instance id='alpha'>
- <test />
- <staging />
- <upgrade rollout='simultaneous' />
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </prod>
- </instance>
- <instance id='beta'>
- <upgrade rollout='simultaneous' />
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </prod>
- </instance>
- <instance id='gamma'>
- <upgrade rollout='separate' />
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </prod>
- </instance>
- </deployment>
- """;
- var appPackage = ApplicationPackageBuilder.fromDeploymentXml(lengthyDeploymentSpec);
- var alpha = tester.newDeploymentContext("t", "a", "alpha");
- var beta = tester.newDeploymentContext("t", "a", "beta");
- var gamma = tester.newDeploymentContext("t", "a", "gamma");
- alpha.submit(appPackage).deploy();
-
- // A version releases, but when the first upgrade has gotten through alpha, beta, and gamma, a newer version has high confidence.
- var version0 = tester.controller().readSystemVersion();
- var version1 = new Version("6.2");
- var version2 = new Version("6.3");
- tester.controllerTester().upgradeSystem(version1);
-
- tester.upgrader().maintain();
- alpha.runJob(systemTest).runJob(stagingTest)
- .runJob(productionUsEast3).runJob(testUsEast3);
- assertEquals(Change.empty(), alpha.instance().change());
-
- tester.upgrader().maintain();
- beta.runJob(productionUsEast3);
- tester.controllerTester().upgradeSystem(version2);
- beta.runJob(testUsEast3);
- assertEquals(Change.empty(), beta.instance().change());
-
- tester.upgrader().maintain();
- assertEquals(Change.of(version2), alpha.instance().change());
- assertEquals(Change.empty(), beta.instance().change());
- assertEquals(Change.of(version1), gamma.instance().change());
- }
-
- @Test
- void testRevisionJoinsUpgradeWithLeadingRollout() {
- var appPackage = new ApplicationPackageBuilder().region("us-central-1")
- .region("us-east-3")
- .region("us-west-1")
- .upgradeRollout("leading")
- .build();
- var app = tester.newDeploymentContext().submit(appPackage).deploy();
-
- // Platform rolls through first production zone.
- var version0 = tester.controller().readSystemVersion();
- var version1 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1);
- tester.clock().advance(Duration.ofMinutes(1));
-
- // Revision starts rolling, and catches up.
- var revision0 = app.lastSubmission();
- app.submit(appPackage);
- var revision1 = app.lastSubmission();
- assertEquals(Change.of(version1).with(revision1.get()), app.instance().change());
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1);
-
- // Upgrade got here first, and has triggered, but is now obsolete.
- app.triggerJobs();
- assertEquals(new Versions(version1, revision0.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsEast3).get().versions());
- assertEquals(RunStatus.running, tester.jobs().last(app.instanceId(), productionUsEast3).get().status());
-
- // Once staging tests verify the joint upgrade, the job is replaced with that.
- app.runJob(stagingTest);
- app.triggerJobs();
- app.jobAborted(productionUsEast3).runJob(productionUsEast3);
- assertEquals(new Versions(version1, revision1.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsEast3).get().versions());
-
- // Platform and revision now proceed together.
- app.triggerJobs();
- assertEquals(new Versions(version1, revision1.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsWest1).get().versions());
- app.runJob(productionUsWest1);
- assertEquals(Change.empty(), app.instance().change());
- }
-
- @Test
- void testRevisionPassesUpgradeWithSimultaneousRollout() {
- var appPackage = new ApplicationPackageBuilder().region("us-central-1")
- .region("us-east-3")
- .region("us-west-1")
- .upgradeRollout("simultaneous")
- .build();
- var app = tester.newDeploymentContext().submit(appPackage).deploy();
-
- // Platform rolls through first production zone.
- var version0 = tester.controller().readSystemVersion();
- var version1 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1);
- tester.clock().advance(Duration.ofMinutes(1));
-
- // Revision starts rolling, and catches up.
- var revision0 = app.lastSubmission();
- app.submit(appPackage);
- var revision1 = app.lastSubmission();
- assertEquals(Change.of(version1).with(revision1.get()), app.instance().change());
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1);
-
- // Upgrade got here first, and has triggered, but is now obsolete.
- app.triggerJobs();
- app.assertRunning(productionUsEast3);
- assertEquals(new Versions(version1, revision0.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsEast3).get().versions());
- assertEquals(RunStatus.running, tester.jobs().last(app.instanceId(), productionUsEast3).get().status());
-
- // Once staging tests verify the joint upgrade, the job is replaced with that.
- app.runJob(systemTest).runJob(stagingTest).runJob(stagingTest);
- app.triggerJobs();
- app.jobAborted(productionUsEast3).runJob(productionUsEast3);
- assertEquals(new Versions(version1, revision1.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsEast3).get().versions());
-
- // Revision now proceeds alone.
- app.triggerJobs();
- assertEquals(new Versions(version0, revision1.get(), Optional.of(version0), revision0),
- tester.jobs().last(app.instanceId(), productionUsWest1).get().versions());
- app.runJob(productionUsWest1);
-
- // Upgrade follows.
- app.triggerJobs();
- assertEquals(new Versions(version1, revision1.get(), Optional.of(version0), revision1),
- tester.jobs().last(app.instanceId(), productionUsWest1).get().versions());
- app.runJob(productionUsWest1);
- assertEquals(Change.empty(), app.instance().change());
- }
-
- @Test
- void mixedDirectAndPipelineJobsInProduction() {
- ApplicationPackage cdPackage = new ApplicationPackageBuilder().region("us-east-3")
- .region("aws-us-east-1a")
- .build();
- ControllerTester wrapped = new ControllerTester(cd);
- wrapped.upgradeSystem(Version.fromString("6.1"));
- wrapped.computeVersionStatus();
-
- DeploymentTester tester = new DeploymentTester(wrapped);
- var app = tester.newDeploymentContext();
-
- app.runJob(productionUsEast3, cdPackage);
- app.submit(cdPackage);
- app.runJob(systemTest);
- // Staging test requires unknown initial version, and is broken.
- tester.controller().applications().deploymentTrigger().forceTrigger(app.instanceId(), productionUsEast3, "user", false, true, true);
- app.runJob(productionUsEast3)
- .abortJob(stagingTest) // Complete failing run.
- .runJob(stagingTest) // Run staging-test for production zone with no prior deployment.
- .runJob(productionAwsUsEast1a);
-
- // Manually deploy to east again, then upgrade the system.
- app.runJob(productionUsEast3, cdPackage);
- var version = new Version("6.2");
- tester.controllerTester().upgradeSystem(version);
- tester.upgrader().maintain();
- // System and staging tests both require unknown versions, and are broken.
- tester.controller().applications().deploymentTrigger().forceTrigger(app.instanceId(), productionUsEast3, "user", false, true, true);
- app.runJob(productionUsEast3)
- .triggerJobs()
- .jobAborted(systemTest)
- .jobAborted(stagingTest)
- .runJob(systemTest) // Run test for aws zone again.
- .runJob(stagingTest) // Run test for aws zone again.
- .runJob(productionAwsUsEast1a);
-
- // Deploy manually again, then submit new package.
- app.runJob(productionUsEast3, cdPackage);
- app.submit(cdPackage);
- app.triggerJobs().runJob(systemTest);
- // Staging test requires unknown initial version, and is broken.
- tester.controller().applications().deploymentTrigger().forceTrigger(app.instanceId(), productionUsEast3, "user", false, true, true);
- app.runJob(productionUsEast3)
- .jobAborted(stagingTest)
- .runJob(stagingTest)
- .runJob(productionAwsUsEast1a);
- }
-
- @Test
- void testsInSeparateInstance() {
- String deploymentSpec =
- """
- <deployment version='1.0' athenz-domain='domain' athenz-service='service'>
- <instance id='canary'>
- <upgrade policy='canary' />
- <test />
- <staging />
- </instance>
- <instance id='default'>
- <prod>
- <region>eu-west-1</region>
- <test>eu-west-1</test>
- </prod>
- </instance>
- </deployment>
- """;
-
- ApplicationPackage applicationPackage = ApplicationPackageBuilder.fromDeploymentXml(deploymentSpec);
- var canary = tester.newDeploymentContext("t", "a", "canary").submit(applicationPackage);
- var conservative = tester.newDeploymentContext("t", "a", "default");
-
- canary.runJob(systemTest)
- .runJob(stagingTest);
- conservative.runJob(productionEuWest1)
- .runJob(testEuWest1);
-
- canary.submit(applicationPackage)
- .runJob(systemTest)
- .runJob(stagingTest);
- tester.outstandingChangeDeployer().run();
- conservative.runJob(productionEuWest1)
- .runJob(testEuWest1);
-
- tester.controllerTester().upgradeSystem(new Version("6.7.7"));
- tester.upgrader().maintain();
-
- canary.runJob(systemTest)
- .runJob(stagingTest);
- tester.upgrader().maintain();
- conservative.runJob(productionEuWest1)
- .runJob(testEuWest1);
-
- }
-
- @Test
- void testEagerTests() {
- var app = tester.newDeploymentContext().submit().deploy();
-
- // Start upgrade, then receive new submission.
- Version version1 = new Version("6.8.9");
- RevisionId build1 = app.lastSubmission().get();
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- app.runJob(stagingTest);
- app.submit();
- RevisionId build2 = app.lastSubmission().get();
- assertNotEquals(build1, build2);
-
- // App now free to start system tests eagerly, for new submission. These should run assuming upgrade succeeds.
- tester.triggerJobs();
- app.assertRunning(stagingTest);
- assertEquals(version1,
- app.instanceJobs().get(stagingTest).lastCompleted().get().versions().targetPlatform());
- assertEquals(build1,
- app.instanceJobs().get(stagingTest).lastCompleted().get().versions().targetRevision());
-
- assertEquals(version1,
- app.instanceJobs().get(stagingTest).lastTriggered().get().versions().sourcePlatform().get());
- assertEquals(build1,
- app.instanceJobs().get(stagingTest).lastTriggered().get().versions().sourceRevision().get());
- assertEquals(version1,
- app.instanceJobs().get(stagingTest).lastTriggered().get().versions().targetPlatform());
- assertEquals(build2,
- app.instanceJobs().get(stagingTest).lastTriggered().get().versions().targetRevision());
-
- // App completes upgrade, and outstanding change is triggered. This should let relevant, running jobs finish.
- app.runJob(systemTest)
- .runJob(productionUsCentral1)
- .runJob(productionUsEast3)
- .runJob(productionUsWest1);
- tester.outstandingChangeDeployer().run();
-
- assertEquals(RunStatus.running, tester.jobs().last(app.instanceId(), stagingTest).get().status());
- app.runJob(stagingTest);
- tester.triggerJobs();
- app.assertNotRunning(stagingTest);
- }
-
- @Test
- void testTriggeringOfIdleTestJobsWhenFirstDeploymentIsOnNewerVersionThanChange() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder().systemTest()
- .stagingTest()
- .region("us-east-3")
- .region("us-west-1")
- .build();
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
- var appToAvoidVersionGC = tester.newDeploymentContext("g", "c", "default").submit().deploy();
-
- Version version2 = new Version("6.8.9");
- Version version3 = new Version("6.9.10");
- tester.controllerTester().upgradeSystem(version2);
- tester.deploymentTrigger().forceChange(appToAvoidVersionGC.instanceId(), Change.of(version2));
- appToAvoidVersionGC.deployPlatform(version2);
-
- // app upgrades first zone to version3, and then the other two to version2.
- tester.controllerTester().upgradeSystem(version3);
- tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(version3));
- app.runJob(systemTest).runJob(stagingTest);
- tester.triggerJobs();
- tester.upgrader().overrideConfidence(version3, VespaVersion.Confidence.broken);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().run();
- assertEquals(Optional.of(version2), app.instance().change().platform());
-
- app.runJob(systemTest)
- .runJob(productionUsEast3)
- .runJob(stagingTest)
- .runJob(productionUsWest1);
-
- assertEquals(version3, app.instanceJobs().get(productionUsEast3).lastSuccess().get().versions().targetPlatform());
- assertEquals(version2, app.instanceJobs().get(productionUsWest1).lastSuccess().get().versions().targetPlatform());
- assertEquals(Map.of(), app.deploymentStatus().jobsToRun());
- assertEquals(Change.empty(), app.instance().change());
- assertEquals(List.of(), tester.jobs().active());
- }
-
- @Test
- void testRetriggerQueue() {
- var app = tester.newDeploymentContext().submit().deploy();
- app.submit();
- tester.triggerJobs();
-
- tester.deploymentTrigger().reTrigger(app.instanceId(), productionUsEast3, null);
- tester.deploymentTrigger().reTriggerOrAddToQueue(app.deploymentIdIn(ZoneId.from("prod", "us-east-3")), null);
- tester.deploymentTrigger().reTriggerOrAddToQueue(app.deploymentIdIn(ZoneId.from("prod", "us-east-3")), null);
-
- List<RetriggerEntry> retriggerEntries = tester.controller().curator().readRetriggerEntries();
- assertEquals(1, retriggerEntries.size());
- }
-
- @Test
- void testOrchestrationWithIncompatibleVersionPairs() {
- Version version1 = new Version("7");
- Version version2 = new Version("8");
- Version version3 = new Version("8.1");
- tester.controllerTester().flagSource().withListFlag(PermanentFlags.INCOMPATIBLE_VERSIONS.id(), List.of("8"), String.class);
-
- // App deploys on version1.
- tester.controllerTester().upgradeSystem(version1);
- DeploymentContext app = tester.newDeploymentContext()
- .submit(new ApplicationPackageBuilder().region("us-east-3")
- .compileVersion(version1)
- .build())
- .deploy();
-
- // System upgrades to version2, and then version3, but the app is not upgraded.
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().run();
- assertEquals(Change.empty(), app.instance().change());
- tester.newDeploymentContext("some", "other", "app")
- .submit(new ApplicationPackageBuilder().region("us-east-3")
- .compileVersion(version2)
- .build())
- .deploy();
-
- tester.controllerTester().upgradeSystem(version3);
- tester.upgrader().run();
- assertEquals(Change.empty(), app.instance().change());
-
- // App compiles against version2, but confidence is broken for the version on new major before app has time to upgrade.
- app.submit(new ApplicationPackageBuilder().region("us-east-3")
- .compileVersion(version2)
- .build());
- tester.upgrader().overrideConfidence(version2, Confidence.normal);
- tester.upgrader().overrideConfidence(version3, Confidence.broken);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().run();
- tester.outstandingChangeDeployer().run();
-
- // App instead deploys to version2.
- app.deploy();
- assertEquals(version2, tester.jobs().last(app.instanceId(), productionUsEast3).get().versions().targetPlatform());
- assertEquals(version2, app.application().revisions().get(tester.jobs().last(app.instanceId(), productionUsEast3).get().versions().targetRevision()).compileVersion().get());
-
- // App specifies version1 in deployment spec, compiles against version1, pins to version1, and then downgrades.
- app.submit(new ApplicationPackageBuilder().region("us-east-3")
- .majorVersion(7)
- .compileVersion(version1)
- .build());
- tester.deploymentTrigger().forceChange(app.instanceId(), app.instance().change().withPlatformPin());
- app.deploy();
- assertEquals(version1, tester.jobs().last(app.instanceId(), productionUsEast3).get().versions().targetPlatform());
- assertEquals(version1, app.application().revisions().get(tester.jobs().last(app.instanceId(), productionUsEast3).get().versions().targetRevision()).compileVersion().get());
-
- // A new app, compiled against version1, is deployed on version1.
- DeploymentContext newApp = tester.newDeploymentContext("new", "app", "default")
- .submit(new ApplicationPackageBuilder().region("us-east-3")
- .compileVersion(version1)
- .build())
- .deploy();
- assertEquals(version1, tester.jobs().last(newApp.instanceId(), productionUsEast3).get().versions().targetPlatform());
- assertEquals(version1, newApp.application().revisions().get(tester.jobs().last(newApp.instanceId(), productionUsEast3).get().versions().targetRevision()).compileVersion().get());
-
- // The new app enters a platform block window, and is pinned to the old platform;
- // the new submission overrides both those settings, as the new revision should roll out regardless.
- tester.atMondayMorning();
- tester.deploymentTrigger().forceChange(newApp.instanceId(), Change.empty().withPlatformPin());
- newApp.submit(new ApplicationPackageBuilder().compileVersion(version2)
- .systemTest()
- .blockChange(false, true, "mon", "0-23", "UTC")
- .region("us-east-3")
- .build());
- RevisionId newRevision = newApp.lastSubmission().get();
-
- assertEquals(Change.of(newRevision).with(version2), newApp.instance().change());
- newApp.deploy();
- assertEquals(version2, tester.jobs().last(newApp.instanceId(), productionUsEast3).get().versions().targetPlatform());
- assertEquals(version2, newApp.application().revisions().get(tester.jobs().last(newApp.instanceId(), productionUsEast3).get().versions().targetRevision()).compileVersion().get());
-
- // New app compiles against old major, and downgrades when a pin is also applied.
- newApp.submit(new ApplicationPackageBuilder().compileVersion(version1)
- .systemTest()
- .region("us-east-3")
- .build());
- newRevision = newApp.lastSubmission().get();
-
- assertEquals(Change.of(newRevision).with(version1), newApp.instance().change());
- tester.triggerJobs();
- newApp.assertNotRunning(systemTest); // Without a pin, the platform won't downgrade, and 8 is incompatible with compiled 7.
-
- tester.outstandingChangeDeployer().run();
- assertEquals(Change.of(newRevision).with(version1), newApp.instance().change());
- tester.upgrader().run();
- assertEquals(Change.of(newRevision).with(version1), newApp.instance().change());
-
- tester.deploymentTrigger().forceChange(newApp.instanceId(), newApp.instance().change().withPlatformPin());
- tester.outstandingChangeDeployer().run();
- assertEquals(Change.of(newRevision).with(version1).withPlatformPin(), newApp.instance().change());
- tester.upgrader().run();
- assertEquals(Change.of(newRevision).with(version1).withPlatformPin(), newApp.instance().change());
-
- newApp.deploy();
- assertEquals(version1, tester.jobs().last(newApp.instanceId(), productionUsEast3).get().versions().targetPlatform());
- assertEquals(version1, newApp.application().revisions().get(tester.jobs().last(newApp.instanceId(), productionUsEast3).get().versions().targetRevision()).compileVersion().get());
- }
-
- @Test
- void testOutdatedMajorIsIllegal() {
- Version version0 = new Version("6.2");
- Version version1 = new Version("7.1");
- tester.controllerTester().upgradeSystem(version0);
- DeploymentContext old = tester.newDeploymentContext("t", "a", "default").submit()
- .runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1);
- old.runJob(JobType.dev("us-east-1"), applicationPackage());
-
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().overrideConfidence(version1, Confidence.high);
- tester.controllerTester().computeVersionStatus();
-
- // New app can't deploy to 6.2
- DeploymentContext app = tester.newDeploymentContext("t", "b", "default");
- assertEquals("platform version 6.2 is not on a current major version in this system",
- assertThrows(IllegalArgumentException.class,
- () -> tester.jobs().deploy(app.instanceId(),
- JobType.dev("us-east-1"),
- Optional.of(version0),
- DeploymentContext.applicationPackage()))
- .getMessage());
-
- // App which already deployed to 6.2 can still do so.
- tester.jobs().deploy(old.instanceId(),
- JobType.dev("us-east-1"),
- Optional.of(version0),
- DeploymentContext.applicationPackage());
-
- app.submit();
- assertEquals("platform version 6.2 is not on a current major version in this system",
- assertThrows(IllegalArgumentException.class,
- () -> tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(version0), false))
- .getMessage());
-
- tester.deploymentTrigger().forceChange(old.instanceId(), Change.of(version0), false);
- tester.deploymentTrigger().cancelChange(old.instanceId(), ALL);
-
- // Not even version incompatibility tricks the system.
- tester.controllerTester().flagSource().withListFlag(PermanentFlags.INCOMPATIBLE_VERSIONS.id(), List.of("7"), String.class);
- assertEquals("compile version 6.2 is incompatible with the current major version of this system",
- assertThrows(IllegalArgumentException.class,
- () ->
- app.submit(new ApplicationPackageBuilder().region("us-central-1").region("us-east-3").region("us-west-1")
- .compileVersion(version0)
- .build()))
- .getMessage());
-
- // Submit new revision on old major
- old.submit(new ApplicationPackageBuilder().region("us-central-1").region("us-east-3").region("us-west-1")
- .compileVersion(version0)
- .build())
- .deploy();
-
- // Upgrade.
- old.submit(new ApplicationPackageBuilder().region("us-central-1").region("us-east-3").region("us-west-1")
- .compileVersion(version1)
- .build())
- .deploy();
-
- // And downgrade again.
- old.submit(new ApplicationPackageBuilder().region("us-central-1").region("us-east-3").region("us-west-1")
- .compileVersion(version0)
- .build());
-
- assertEquals(Change.of(version0).with(old.lastSubmission().get()), old.instance().change());
-
- // An operator can still trigger roll-out of the otherwise illegal submission.
- tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(app.lastSubmission().get()));
- assertEquals(Change.of(app.lastSubmission().get()), app.instance().change());
- }
-
- @Test
- void operatorMayForceUnknownVersion() {
- Version oldVersion = Version.fromString("6");
- Version currentVersion = Version.fromString("7");
- tester.controllerTester().flagSource().withListFlag(PermanentFlags.INCOMPATIBLE_VERSIONS.id(), List.of("7"), String.class);
- tester.controllerTester().upgradeSystem(currentVersion);
- assertEquals(List.of(currentVersion),
- tester.controller().readVersionStatus().versions().stream().map(VespaVersion::versionNumber).toList());
-
- DeploymentContext app = tester.newDeploymentContext();
- assertEquals("compile version 6 is incompatible with the current major version of this system",
- assertThrows(IllegalArgumentException.class,
- () -> app.submit(new ApplicationPackageBuilder().region("us-east-3")
- .majorVersion(6)
- .compileVersion(oldVersion)
- .build()))
- .getMessage());
-
- tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(oldVersion).with(app.application().revisions().last().get().id()).withPlatformPin());
- app.deploy();
- assertEquals(oldVersion, app.deployment(ZoneId.from("prod", "us-east-3")).version());
-
- tester.controllerTester().computeVersionStatus();
- assertEquals(List.of(oldVersion, currentVersion),
- tester.controller().readVersionStatus().versions().stream().map(VespaVersion::versionNumber).toList());
- }
-
- @Test
- void testInitialDeploymentPlatform() {
- Version version0 = tester.controllerTester().controller().readSystemVersion();
- Version version1 = new Version("6.2");
- Version version2 = new Version("6.3");
- assertEquals(version0, tester.newDeploymentContext("t", "a1", "default").submit().deploy().application().oldestDeployedPlatform().get());
-
- // A new version, with normal confidence, is the default for a new app.
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().overrideConfidence(version1, Confidence.normal);
- tester.controllerTester().computeVersionStatus();
- assertEquals(version1, tester.newDeploymentContext("t", "a2", "default").submit().deploy().application().oldestDeployedPlatform().get());
-
- // A newer version has broken confidence, leaving the previous version as the default.
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().overrideConfidence(version2, Confidence.broken);
- tester.controllerTester().computeVersionStatus();
- assertEquals(version1, tester.newDeploymentContext("t", "a3", "default").submit().deploy().application().oldestDeployedPlatform().get());
-
- DeploymentContext dev1 = tester.newDeploymentContext("t", "d1", "default");
- DeploymentContext dev2 = tester.newDeploymentContext("t", "d2", "default");
- assertEquals(version1, dev1.runJob(JobType.dev("us-east-1"), DeploymentContext.applicationPackage()).deployment(ZoneId.from("dev", "us-east-1")).version());
-
- DeploymentUpgrader devUpgrader = new DeploymentUpgrader(tester.controller(), Duration.ofHours(1));
- for (int i = 0; i < 24; i++) {
- tester.clock().advance(Duration.ofHours(1));
- devUpgrader.run();
- }
- dev1.assertNotRunning(JobType.dev("us-east-1"));
-
- // Normal confidence lets the newest version be the default again.
- tester.upgrader().overrideConfidence(version2, Confidence.normal);
- tester.controllerTester().computeVersionStatus();
- assertEquals(version2, tester.newDeploymentContext("t", "a4", "default").submit().deploy().application().oldestDeployedPlatform().get());
- assertEquals(version1, dev1.runJob(JobType.dev("us-east-1"), DeploymentContext.applicationPackage()).deployment(ZoneId.from("dev", "us-east-1")).version());
- assertEquals(version2, dev2.runJob(JobType.dev("us-east-1"), DeploymentContext.applicationPackage()).deployment(ZoneId.from("dev", "us-east-1")).version());
-
- for (int i = 0; i < 24; i++) {
- tester.clock().advance(Duration.ofHours(1));
- devUpgrader.run();
- }
- dev1.assertRunning(JobType.dev("us-east-1"));
- dev1.runJob(JobType.dev("us-east-1"));
- assertEquals(version2, dev1.deployment(ZoneId.from("dev", "us-east-1")).version());
- }
-
- @Test
- void testInstanceWithOnlySystemTestInTwoClouds() {
- String spec = """
- <deployment>
- <instance id='tests'>
- <test />
- <upgrade revision-target='next' />
- </instance>
- <instance id='main'>
- <prod>
- <region>us-east-3</region>
- <region>alpha-centauri</region>
- </prod>
- <upgrade revision-target='next' />
- </instance>
- </deployment>
- """;
-
- RegionName alphaCentauri = RegionName.from("alpha-centauri");
- ZoneApiMock.Builder builder = ZoneApiMock.newBuilder().withCloud("centauri").withSystem(tester.controller().system());
- ZoneApi testAlphaCentauri = builder.with(ZoneId.from(Environment.test, alphaCentauri)).build();
- ZoneApi stagingAlphaCentauri = builder.with(ZoneId.from(Environment.staging, alphaCentauri)).build();
- ZoneApi prodAlphaCentauri = builder.with(ZoneId.from(prod, alphaCentauri)).build();
-
- tester.controllerTester().zoneRegistry().addZones(testAlphaCentauri, stagingAlphaCentauri, prodAlphaCentauri);
- tester.controllerTester().setRoutingMethod(tester.controllerTester().zoneRegistry().zones().all().ids(), RoutingMethod.sharedLayer4);
- tester.configServer().bootstrap(tester.controllerTester().zoneRegistry().zones().all().ids(), SystemApplication.notController());
-
- ApplicationPackage appPackage = ApplicationPackageBuilder.fromDeploymentXml(spec);
- DeploymentContext tests = tester.newDeploymentContext("tenant", "application", "tests");
- DeploymentContext main = tester.newDeploymentContext("tenant", "application", "main");
- Version version1 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version1);
- tests.submit(appPackage);
- JobId systemTestJob = new JobId(tests.instanceId(), systemTest);
- JobId stagingTestJob = new JobId(tests.instanceId(), stagingTest);
- JobId mainJob = new JobId(main.instanceId(), productionUsEast3);
- JobId centauriJob = new JobId(main.instanceId(), JobType.deploymentTo(prodAlphaCentauri.getId()));
- JobType centuariTest = JobType.systemTest(tester.controllerTester().zoneRegistry(), CloudName.from("centauri"));
- JobType centuariStaging = JobType.stagingTest(tester.controllerTester().zoneRegistry(), CloudName.from("centauri"));
-
- assertEquals(Set.of(systemTestJob, stagingTestJob, mainJob, centauriJob), tests.deploymentStatus().jobsToRun().keySet());
- tests.runJob(systemTest).runJob(stagingTest).triggerJobs();
-
- assertEquals(Set.of(systemTestJob, stagingTestJob, mainJob, centauriJob), tests.deploymentStatus().jobsToRun().keySet());
- tests.triggerJobs();
- assertEquals(3, tester.jobs().active().size());
-
- tests.runJob(centuariTest);
- tester.outstandingChangeDeployer().run();
-
- assertEquals(2, tester.jobs().active().size());
- main.assertRunning(productionUsEast3);
-
- tests.runJob(centuariStaging);
- main.runJob(productionUsEast3).runJob(centauriJob.type());
-
- assertEquals(Change.empty(), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(), tests.deploymentStatus().jobsToRun().keySet());
-
- // Versions 2 and 3 become available.
- // Tests instance fails on 2, then updates to 3.
- // Version 2 should not be a target for either instance.
- // Version 2 should also not be possible to set as a forced target for the tests instance.
- Version version2 = new Version("6.3");
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().run();
- tester.triggerJobs();
-
- assertEquals(Change.of(version2), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(systemTestJob), tests.deploymentStatus().jobsToRun().keySet());
- assertEquals(2, tests.deploymentStatus().jobsToRun().get(systemTestJob).size());
-
- Version version3 = new Version("6.4");
- tester.controllerTester().upgradeSystem(version3);
- tests.runJob(systemTest) // Success in default cloud.
- .failDeployment(centuariTest); // Failure in centauri cloud.
- tester.upgrader().run();
-
- assertEquals(Change.of(version3), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(systemTestJob), tests.deploymentStatus().jobsToRun().keySet());
-
- tests.runJob(systemTest).runJob(centuariTest);
- tester.upgrader().run();
- tests.runJob(stagingTest).runJob(centuariStaging);
-
- assertEquals(Change.empty(), tests.instance().change());
- assertEquals(Change.of(version3), main.instance().change());
- assertEquals(Set.of(mainJob, centauriJob), tests.deploymentStatus().jobsToRun().keySet());
-
- main.runJob(productionUsEast3);
- main.runJob(centauriJob.type());
-
- assertEquals(Change.empty(), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(), tests.deploymentStatus().jobsToRun().keySet());
-
- tester.deploymentTrigger().forceChange(tests.instanceId(), Change.of(version2));
-
- assertEquals(Change.empty(), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(), tests.deploymentStatus().jobsToRun().keySet());
-
- // Revisions 2 and 3 become available.
- // Tests instance fails on 2, then update to 3.
- // Revision 2 should not be a target for either instance.
- // Revision 2 should also not be possible to set as a forced target for the tests instance.
- tests.submit(appPackage);
- Optional<RevisionId> revision2 = tests.lastSubmission();
-
- assertEquals(Change.of(revision2.get()), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(systemTestJob), tests.deploymentStatus().jobsToRun().keySet());
- assertEquals(2, tests.deploymentStatus().jobsToRun().get(systemTestJob).size());
-
- tests.submit(appPackage);
- Optional<RevisionId> revision3 = tests.lastSubmission();
-
- assertEquals(Change.of(revision2.get()), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(systemTestJob), tests.deploymentStatus().jobsToRun().keySet());
-
- tests.failDeployment(systemTest);
- tester.outstandingChangeDeployer().run();
-
- assertEquals(Change.of(revision3.get()), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(systemTestJob), tests.deploymentStatus().jobsToRun().keySet());
-
- tests.runJob(systemTest);
- assertEquals(Change.of(revision3.get()), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(systemTestJob, stagingTestJob), tests.deploymentStatus().jobsToRun().keySet());
-
- tester.outstandingChangeDeployer().run();
- tester.outstandingChangeDeployer().run();
- tests.runJob(stagingTest);
-
- assertEquals(Change.of(revision3.get()), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(systemTestJob, stagingTestJob), tests.deploymentStatus().jobsToRun().keySet());
-
- tests.runJob(centuariTest);
- tester.outstandingChangeDeployer().run();
- tester.outstandingChangeDeployer().run();
-
- assertEquals(Change.empty(), tests.instance().change());
- assertEquals(Change.of(revision3.get()), main.instance().change());
- assertEquals(Set.of(stagingTestJob, mainJob, centauriJob), tests.deploymentStatus().jobsToRun().keySet());
-
- tests.runJob(centuariStaging);
-
- assertEquals(Change.empty(), tests.instance().change());
- assertEquals(Change.of(revision3.get()), main.instance().change());
- assertEquals(Set.of(mainJob, centauriJob), tests.deploymentStatus().jobsToRun().keySet());
-
- main.runJob(productionUsEast3);
- main.runJob(centauriJob.type());
- tester.outstandingChangeDeployer().run();
-
- assertEquals(Change.empty(), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(), tests.deploymentStatus().jobsToRun().keySet());
-
- tester.deploymentTrigger().forceChange(tests.instanceId(), Change.of(revision2.get()));
-
- assertEquals(Change.empty(), tests.instance().change());
- assertEquals(Change.empty(), main.instance().change());
- assertEquals(Set.of(), tests.deploymentStatus().jobsToRun().keySet());
- }
-
- @Test
- void testInstancesWithMultipleClouds() {
- String spec = """
- <deployment>
- <parallel>
- <instance id='separate'>
- <test />
- <staging />
- <prod>
- <region>alpha-centauri</region>
- </prod>
- </instance>
- <instance id='independent'>
- <test />
- </instance>
- <steps>
- <parallel>
- <instance id='alpha'>
- <test />
- <prod>
- <region>us-east-3</region>
- </prod>
- </instance>
- <instance id='beta'>
- <test />
- <prod>
- <region>alpha-centauri</region>
- </prod>
- </instance>
- <instance id='gamma'>
- <test />
- </instance>
- </parallel>
- <instance id='nu'>
- <staging />
- </instance>
- <instance id='omega'>
- <prod>
- <region>alpha-centauri</region>
- </prod>
- </instance>
- </steps>
- <instance id='dependent'>
- <prod>
- <region>us-east-3</region>
- </prod>
- </instance>
- </parallel>
- </deployment>
- """;
-
- RegionName alphaCentauri = RegionName.from("alpha-centauri");
- ZoneApiMock.Builder builder = ZoneApiMock.newBuilder().withCloud("centauri").withSystem(tester.controller().system());
- ZoneApi testAlphaCentauri = builder.with(ZoneId.from(Environment.test, alphaCentauri)).build();
- ZoneApi stagingAlphaCentauri = builder.with(ZoneId.from(Environment.staging, alphaCentauri)).build();
- ZoneApi prodAlphaCentauri = builder.with(ZoneId.from(prod, alphaCentauri)).build();
-
- tester.controllerTester().zoneRegistry().addZones(testAlphaCentauri, stagingAlphaCentauri, prodAlphaCentauri);
- tester.controllerTester().setRoutingMethod(tester.controllerTester().zoneRegistry().zones().all().ids(), RoutingMethod.sharedLayer4);
- tester.configServer().bootstrap(tester.controllerTester().zoneRegistry().zones().all().ids(), SystemApplication.notController());
-
- ApplicationPackage appPackage = ApplicationPackageBuilder.fromDeploymentXml(spec);
- DeploymentContext alpha = tester.newDeploymentContext("tenant", "application", "alpha").submit(appPackage).deploy();
- DeploymentContext beta = tester.newDeploymentContext("tenant", "application", "beta");
- DeploymentContext gamma = tester.newDeploymentContext("tenant", "application", "gamma");
- DeploymentContext nu = tester.newDeploymentContext("tenant", "application", "nu");
- DeploymentContext omega = tester.newDeploymentContext("tenant", "application", "omega");
- DeploymentContext separate = tester.newDeploymentContext("tenant", "application", "separate");
- DeploymentContext independent = tester.newDeploymentContext("tenant", "application", "independent");
- DeploymentContext dependent = tester.newDeploymentContext("tenant", "application", "dependent");
- alpha.submit(appPackage);
- Map<JobId, List<DeploymentStatus.Job>> jobs = alpha.deploymentStatus().jobsToRun();
-
- JobType centauriTest = JobType.systemTest(tester.controller().zoneRegistry(), CloudName.from("centauri"));
- JobType centauriStaging = JobType.stagingTest(tester.controller().zoneRegistry(), CloudName.from("centauri"));
- JobType centauriProd = JobType.deploymentTo(ZoneId.from(prod, alphaCentauri));
- assertQueued("separate", jobs, systemTest, centauriTest);
- assertQueued("separate", jobs, stagingTest, centauriStaging);
- assertQueued("independent", jobs, systemTest, centauriTest);
- assertQueued("alpha", jobs, systemTest);
- assertQueued("beta", jobs, centauriTest);
- assertQueued("gamma", jobs, centauriTest);
-
- // Once alpha runs its default system test, it also runs the centauri system test, as omega depends on it.
- alpha.runJob(systemTest);
- assertQueued("alpha", alpha.deploymentStatus().jobsToRun(), centauriTest);
-
- // Run tests, and see production jobs are triggered as they are verified.
- for (DeploymentContext app : List.of(alpha, beta, gamma, nu, omega, separate, independent, dependent))
- tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(alpha.lastSubmission().get()));
-
- // Missing separate staging test.
- alpha.triggerJobs().assertNotRunning(productionUsEast3);
-
- beta.runJob(centauriTest);
- // Missing separate centauri staging.
- beta.triggerJobs().assertNotRunning(centauriProd);
-
- gamma.runJob(centauriTest);
-
- // Missing alpha centauri test, and nu centauri staging.
- omega.triggerJobs().assertNotRunning(centauriProd);
- alpha.runJob(centauriTest);
- omega.triggerJobs().assertNotRunning(centauriProd);
- nu.runJob(centauriStaging);
- omega.triggerJobs().assertRunning(centauriProd);
-
- separate.triggerJobs().assertNotRunning(centauriProd);
-
- separate.runJob(centauriStaging);
- separate.triggerJobs().assertNotRunning(centauriProd);
- beta.triggerJobs().assertRunning(centauriProd);
-
- separate.runJob(centauriTest);
- separate.triggerJobs().assertRunning(centauriProd);
-
- dependent.triggerJobs().assertNotRunning(productionUsEast3);
-
- separate.runJob(systemTest).runJob(stagingTest).triggerJobs();
- dependent.triggerJobs().assertRunning(productionUsEast3);
- alpha.triggerJobs().assertRunning(productionUsEast3);
-
- separate.runJob(centauriProd);
- alpha.runJob(productionUsEast3);
- beta.runJob(centauriProd);
- omega.runJob(centauriProd);
- dependent.runJob(productionUsEast3);
- independent.runJob(centauriTest).runJob(systemTest);
- assertEquals(Map.of(), alpha.deploymentStatus().jobsToRun());
- }
-
- private static void assertQueued(String instance, Map<JobId, List<DeploymentStatus.Job>> jobs, JobType... expected) {
- List<DeploymentStatus.Job> queued = jobs.get(new JobId(ApplicationId.from("tenant", "application", instance), expected[0]));
- Set<ZoneId> remaining = new HashSet<>();
- for (JobType ex : expected) remaining.add(ex.zone());
- for (DeploymentStatus.Job q : queued)
- if ( ! remaining.remove(q.type().zone()))
- fail("unexpected queued job for " + instance + ": " + q.type());
- if ( ! remaining.isEmpty())
- fail("expected tests for " + instance + " were not queued in : " + remaining);
- }
-
- @Test
- void testNoTests() {
- DeploymentContext app = tester.newDeploymentContext();
- app.submit(new ApplicationPackageBuilder().systemTest().region("us-east-3").build());
-
- // Declared tests must have run actual tests to succeed.
- app.failTests(systemTest, true);
- assertFalse(tester.jobs().last(app.instanceId(), systemTest).get().hasSucceeded());
- app.failTests(stagingTest, true);
- assertTrue(tester.jobs().last(app.instanceId(), stagingTest).get().hasSucceeded());
- }
-
- @Test
- void testBrokenApplication() {
- DeploymentContext app = tester.newDeploymentContext();
- app.submit().runJob(systemTest).failDeployment(stagingTest).failDeployment(stagingTest);
- tester.clock().advance(Duration.ofDays(31));
- tester.outstandingChangeDeployer().run();
- assertEquals(OptionalLong.empty(), app.application().projectId());
-
- app.assertNotRunning(stagingTest);
- tester.triggerJobs();
- app.assertNotRunning(stagingTest);
- assertEquals(4, app.deploymentStatus().jobsToRun().size());
-
- app.submit().runJob(systemTest).failDeployment(stagingTest);
- tester.clock().advance(Duration.ofDays(20));
- app.submit().runJob(systemTest).failDeployment(stagingTest);
- tester.clock().advance(Duration.ofDays(20));
- tester.outstandingChangeDeployer().run();
- assertEquals(OptionalLong.of(1000), app.application().projectId());
- tester.clock().advance(Duration.ofDays(20));
- tester.outstandingChangeDeployer().run();
- assertEquals(OptionalLong.empty(), app.application().projectId());
-
- app.assertNotRunning(stagingTest);
- tester.triggerJobs();
- app.assertNotRunning(stagingTest);
- assertEquals(4, app.deploymentStatus().jobsToRun().size());
-
- app.submit().runJob(systemTest).runJob(stagingTest).failDeployment(productionUsCentral1);
- tester.clock().advance(Duration.ofDays(31));
- tester.outstandingChangeDeployer().run();
- assertEquals(OptionalLong.empty(), app.application().projectId());
-
- app.assertNotRunning(productionUsCentral1);
- tester.triggerJobs();
- app.assertNotRunning(productionUsCentral1);
- assertEquals(3, app.deploymentStatus().jobsToRun().size());
-
- app.submit().runJob(systemTest).runJob(stagingTest).timeOutConvergence(productionUsCentral1);
- tester.clock().advance(Duration.ofDays(31));
- tester.outstandingChangeDeployer().run();
- assertEquals(OptionalLong.of(1000), app.application().projectId());
-
- app.assertNotRunning(productionUsCentral1);
- tester.triggerJobs();
- app.assertRunning(productionUsCentral1);
- }
-
- @Test
- void testJobNames() {
- ZoneRegistryMock zones = new ZoneRegistryMock(SystemName.main);
- List<ZoneApi> existing = new ArrayList<>(zones.zones().all().zones());
- existing.add(ZoneApiMock.newBuilder().withCloud("pink-clouds").withId("test.zone").build());
- zones.setZones(existing);
-
- JobType defaultSystemTest = JobType.systemTest(zones, CloudName.DEFAULT);
- JobType pinkSystemTest = JobType.systemTest(zones, CloudName.from("pink-clouds"));
-
- // Job name is identity, used for looking up run history, etc..
- assertEquals(defaultSystemTest, pinkSystemTest);
-
- assertEquals(defaultSystemTest, JobType.systemTest(zones, null));
- assertEquals(defaultSystemTest, JobType.systemTest(zones, CloudName.from("dark-clouds")));
- assertEquals(defaultSystemTest, JobType.fromJobName("system-test", zones));
-
- assertEquals(ZoneId.from("test", "us-east-1"), defaultSystemTest.zone());
- assertEquals(ZoneId.from("staging", "us-east-3"), JobType.stagingTest(zones, zones.systemZone().getCloudName()).zone());
-
- assertEquals(ZoneId.from("test", "zone"), pinkSystemTest.zone());
- assertEquals(ZoneId.from("staging", "us-east-3"), JobType.stagingTest(zones, CloudName.from("pink-clouds")).zone());
-
- assertThrows(IllegalStateException.class, JobType.systemTest(zones, null)::zone);
- assertThrows(IllegalStateException.class, JobType.fromJobName("system-test", zones)::zone);
- assertThrows(IllegalStateException.class, JobType.fromJobName("staging-test", zones)::zone);
- }
-
- @Test
- void testOrderOfTests() {
- String deploymentXml = """
- <deployment version="1.0">
- <test/>
- <staging/>
- <block-change days="fri" hours="0-23" time-zone="UTC" />
- <prod>
- <region>us-east-3</region>
- <delay hours="1"/>
- <test>us-east-3</test>
- <region>us-west-1</region>
- </prod>
- </deployment>""";
-
- Version version1 = new Version("7.1");
- tester.controllerTester().upgradeSystem(version1);
- ApplicationPackage applicationPackage = ApplicationPackageBuilder.fromDeploymentXml(deploymentXml);
- tester.clock().setInstant(Instant.EPOCH.plusSeconds(8 * 60 * 60)); // Thursday morning.
- DeploymentContext app = tester.newDeploymentContext().submit(applicationPackage);
- RevisionId revision1 = app.lastSubmission().get();
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsEast3);
- tester.clock().advance(Duration.ofHours(1));
- app.runJob(testUsEast3).runJob(productionUsWest1);
- assertEquals(Change.empty(), app.instance().change());
-
- tester.clock().advance(Duration.ofDays(1)); // Enter block window.
- app.submit(applicationPackage);
- assertEquals(Change.empty(), app.instance().change());
-
- Version version2 = new Version("7.2");
- RevisionId revision2 = app.lastSubmission().get();
-
- app.runJob(systemTest).runJob(stagingTest);
- app.triggerJobs().assertNotRunning(productionUsEast3);
- tester.controllerTester().upgradeSystem(version2);
- tester.clock().advance(Duration.ofDays(1)); // Leave block window.
- tester.upgrader().run();
- tester.outstandingChangeDeployer().run();
- assertEquals(Change.of(revision2).with(version2), app.instance().change());
- app.runJob(systemTest).runJob(stagingTest);
- app.runJob(productionUsEast3);
- app.triggerJobs();
- app.assertNotRunning(productionUsEast3); // Platform upgrade should not start before test is done with revision.
- tester.clock().advance(Duration.ofHours(1));
- app.triggerJobs();
- app.assertNotRunning(productionUsEast3); // Platform upgrade should not start before test is done with revision.
- app.runJob(testUsEast3);
- app.runJob(productionUsEast3)
- .runJob(productionUsWest1);
- tester.clock().advance(Duration.ofHours(1));
- app.runJob(testUsEast3);
- app.runJob(productionUsWest1);
- assertEquals(Change.empty(), app.instance().change());
- }
-
- @Test
- @Disabled // For benchmarking, not a test
- void miniBenchmark() {
- String spec = """
- <deployment version="1.0">
- <parallel>
- <instance id="instance0">
- <test tester-flavor="d-8-16-10" />
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- </instance>
- <instance id="instance1">
- <test tester-flavor="d-8-16-10" />
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- </instance>
- <instance id="instance2">
- <test tester-flavor="d-8-16-10" />
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- </instance>
- <instance id="instance3">
- <test tester-flavor="d-8-16-10" />
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- </instance>
- <instance id="stress">
- <staging />
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- </instance>
- </parallel>
- <instance id="beta1">
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- <block-change version="true" revision="false" days="sat" hours="0-23" time-zone="UTC" />
- <upgrade revision-change='when-clear' rollout='separate' revision-target='next' policy='conservative'/>
- <prod>
- <parallel>
- <steps>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </steps>
- <steps>
- <region>us-west-1</region>
- <test>us-west-1</test>
- </steps>
- <steps>
- <region>eu-west-1</region>
- <test>eu-west-1</test>
- </steps>
- <steps>
- <region>us-central-1</region>
- <test>us-central-1</test>
- </steps>
- </parallel>
- </prod>
- </instance>
- <instance id="gamma5">
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- <block-change version="true" revision="false" days="sat" hours="0-23" time-zone="UTC" />
- <upgrade revision-change='when-clear' rollout='separate' revision-target='next' policy='conservative'/>
- <prod>
- <parallel>
- <steps>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </steps>
- <steps>
- <region>us-west-1</region>
- <test>us-west-1</test>
- </steps>
- <steps>
- <region>eu-west-1</region>
- <test>eu-west-1</test>
- </steps>
- <steps>
- <region>us-central-1</region>
- <test>us-central-1</test>
- </steps>
- </parallel>
- </prod>
- </instance>
- <instance id="delta21">
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- <block-change version="true" revision="false" days="sat" hours="0-23" time-zone="UTC" />
- <upgrade revision-change='when-clear' rollout='separate' revision-target='next' policy='conservative'/>
- <prod>
- <parallel>
- <steps>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </steps>
- <steps>
- <region>us-west-1</region>
- <test>us-west-1</test>
- </steps>
- <steps>
- <region>eu-west-1</region>
- <test>eu-west-1</test>
- </steps>
- <steps>
- <region>us-central-1</region>
- <test>us-central-1</test>
- </steps>
- </parallel>
- </prod>
- </instance>
- <instance id="prod21a">
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- <block-change version="true" revision="false" days="sat" hours="0-23" time-zone="UTC" />
- <upgrade revision-change='when-clear' rollout='separate' revision-target='next' policy='conservative'/>
- <prod>
- <parallel>
- <steps>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </steps>
- <steps>
- <region>us-west-1</region>
- <test>us-west-1</test>
- </steps>
- <steps>
- <region>eu-west-1</region>
- <test>eu-west-1</test>
- </steps>
- <steps>
- <region>us-central-1</region>
- <test>us-central-1</test>
- </steps>
- </parallel>
- </prod>
- </instance>
- <instance id="prod21b">
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- <block-change version="true" revision="false" days="sat" hours="0-23" time-zone="UTC" />
- <upgrade revision-change='when-clear' rollout='separate' revision-target='next' policy='conservative'/>
- <prod>
- <parallel>
- <steps>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </steps>
- <steps>
- <region>us-west-1</region>
- <test>us-west-1</test>
- </steps>
- <steps>
- <region>eu-west-1</region>
- <test>eu-west-1</test>
- </steps>
- <steps>
- <region>us-central-1</region>
- <test>us-central-1</test>
- </steps>
- </parallel>
- </prod>
- </instance>
- <instance id="prod21c">
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- <block-change version="true" revision="false" days="sat" hours="0-23" time-zone="UTC" />
- <upgrade revision-change='when-clear' rollout='separate' revision-target='next' policy='conservative'/>
- <prod>
- <parallel>
- <steps>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </steps>
- <steps>
- <region>us-west-1</region>
- <test>us-west-1</test>
- </steps>
- <steps>
- <region>eu-west-1</region>
- <test>eu-west-1</test>
- </steps>
- <steps>
- <region>us-central-1</region>
- <test>us-central-1</test>
- </steps>
- </parallel>
- </prod>
- </instance>
- <instance id="cd10">
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- <block-change version="true" revision="false" days="sat" hours="0-23" time-zone="UTC" />
- <upgrade revision-change='when-clear' rollout='separate' revision-target='next' policy='conservative'/>
- <prod>
- <parallel>
- <steps>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </steps>
- <steps>
- <region>us-west-1</region>
- <test>us-west-1</test>
- </steps>
- <steps>
- <region>eu-west-1</region>
- <test>eu-west-1</test>
- </steps>
- <steps>
- <region>us-central-1</region>
- <test>us-central-1</test>
- </steps>
- </parallel>
- </prod>
- </instance>
- <instance id="prod1">
- <block-change version="true" revision="false" days="mon-fri,sun" hours="4-23" time-zone="UTC" />
- <block-change version="true" revision="false" days="sat" hours="0-23" time-zone="UTC" />
- <upgrade revision-change='when-clear' rollout='separate' revision-target='next' policy='conservative'/>
- <prod>
- <parallel>
- <steps>
- <region>us-east-3</region>
- <test>us-east-3</test>
- </steps>
- <steps>
- <region>us-west-1</region>
- <test>us-west-1</test>
- </steps>
- <steps>
- <region>eu-west-1</region>
- <test>eu-west-1</test>
- </steps>
- <steps>
- <region>us-central-1</region>
- <test>us-central-1</test>
- </steps>
- </parallel>
- </prod>
- </instance>
- </deployment>""";
- tester.newDeploymentContext("t", "a", "prod1").submit(ApplicationPackageBuilder.fromDeploymentXml(spec)).deploy();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
deleted file mode 100644
index 94cf016b46b..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/InternalStepRunnerTest.java
+++ /dev/null
@@ -1,566 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.AthenzService;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.slime.Inspector;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud.Status;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.maintenance.JobRunner;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type.error;
-import static com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type.info;
-import static com.yahoo.vespa.hosted.controller.api.integration.LogEntry.Type.warning;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.applicationPackage;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTester.instanceId;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.deploymentFailed;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.noTests;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.quotaExceeded;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.success;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.failed;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author jonmv
- * @author freva
- */
-public class InternalStepRunnerTest {
-
- private DeploymentTester tester;
- private DeploymentContext app;
-
- @BeforeEach
- public void setup() {
- tester = new DeploymentTester();
- app = tester.newDeploymentContext();
- }
-
- private SystemName system() {
- return tester.controller().system();
- }
-
- @Test
- public void canRegisterAndRunDirectly() {
- app.submit().deploy();
- }
-
- @Test
- public void testerHasAthenzIdentity() {
- app.submit();
- tester.triggerJobs();
- tester.runner().run();
- DeploymentSpec spec = tester.configServer()
- .application(app.testerId().id(), DeploymentContext.stagingTest.zone()).get()
- .applicationPackage().deploymentSpec();
- assertTrue(spec.instance(app.testerId().id().instance()).isPresent());
- assertEquals("domain", spec.athenzDomain().get().value());
- assertEquals("service", spec.athenzService().get().value());
- }
-
- @Test
- public void retriesDeploymentForOneHour() {
- RuntimeException exception = new ConfigServerException(ConfigServerException.ErrorCode.APPLICATION_LOCK_FAILURE,
- "Exception to retry",
- "test failure");
- tester.configServer().throwOnNextPrepare(exception);
- tester.jobs().deploy(app.instanceId(), DeploymentContext.devUsEast1, Optional.empty(), applicationPackage());
- assertEquals(unfinished, tester.jobs().last(app.instanceId(), DeploymentContext.devUsEast1).get().stepStatuses().get(Step.deployReal));
-
- tester.configServer().throwOnNextPrepare(exception);
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().last(app.instanceId(), DeploymentContext.devUsEast1).get().stepStatuses().get(Step.deployReal));
-
- tester.clock().advance(Duration.ofHours(1).plusSeconds(1));
- tester.configServer().throwOnNextPrepare(exception);
- tester.runner().run();
- assertEquals(failed, tester.jobs().last(app.instanceId(), DeploymentContext.devUsEast1).get().stepStatuses().get(Step.deployReal));
- assertEquals(deploymentFailed, tester.jobs().last(app.instanceId(), DeploymentContext.devUsEast1).get().status());
- }
-
- @Test
- public void restartsServicesAndWaitsForRestartAndReboot() {
- RunId id = app.newRun(DeploymentContext.productionUsCentral1);
- ZoneId zone = id.type().zone();
- HostName host = tester.configServer().hostFor(instanceId, zone);
-
- tester.runner().run();
- assertEquals(succeeded, tester.jobs().run(id).stepStatuses().get(Step.deployReal));
-
- tester.configServer().convergeServices(app.instanceId(), zone);
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.installReal));
-
- tester.configServer().nodeRepository().doRestart(app.deploymentIdIn(zone), Optional.of(host));
- tester.configServer().nodeRepository().requestReboot(app.deploymentIdIn(zone), Optional.of(host));
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.installReal));
-
- tester.clock().advance(InternalStepRunner.Timeouts.of(system()).noNodesDown().plus(Duration.ofSeconds(1)));
- tester.runner().run();
- assertEquals(installationFailed, tester.jobs().run(id).status());
- }
-
- @Test
- public void waitsForEndpointsAndTimesOut() {
- app.newRun(DeploymentContext.systemTest);
-
- // Tester endpoint fails to show up for staging tests, and the real deployment for system tests.
- var testZone = DeploymentContext.systemTest.zone();
- var stagingZone = DeploymentContext.stagingTest.zone();
- tester.newDeploymentContext(app.testerId().id())
- .deferLoadBalancerProvisioningIn(testZone.environment());
- tester.newDeploymentContext(app.instanceId())
- .deferLoadBalancerProvisioningIn(stagingZone.environment());
-
- tester.runner().run();
- tester.configServer().convergeServices(app.instanceId(), DeploymentContext.stagingTest.zone());
- tester.runner().run();
- tester.configServer().convergeServices(app.instanceId(), DeploymentContext.systemTest.zone());
- tester.configServer().convergeServices(app.testerId().id(), DeploymentContext.systemTest.zone());
- tester.configServer().convergeServices(app.instanceId(), DeploymentContext.stagingTest.zone());
- tester.configServer().convergeServices(app.testerId().id(), DeploymentContext.stagingTest.zone());
- tester.runner().run();
-
- tester.clock().advance(InternalStepRunner.Timeouts.of(system()).endpoint().plus(Duration.ofSeconds(1)));
- tester.runner().run();
- assertEquals(failed, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installReal));
- }
-
- @Test
- public void timesOutWithoutInstallationProgress() {
- tester.controllerTester().upgradeSystem(new Version("7.1"));
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- app.newRun(DeploymentContext.systemTest);
-
- // Node is down too long in system test, and no nodes go down in staging.
- tester.runner().run();
- tester.configServer().setVersion(tester.controller().readSystemVersion(), app.testerId().id(), DeploymentContext.systemTest.zone());
- tester.configServer().convergeServices(app.testerId().id(), DeploymentContext.systemTest.zone());
- tester.configServer().setVersion(tester.controller().readSystemVersion(), app.testerId().id(), DeploymentContext.stagingTest.zone());
- tester.configServer().convergeServices(app.testerId().id(), DeploymentContext.stagingTest.zone());
- tester.runner().run();
- assertEquals(succeeded, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installTester));
- assertEquals(succeeded, tester.jobs().last(app.instanceId(), DeploymentContext.stagingTest).get().stepStatuses().get(Step.installTester));
-
- Node systemTestNode = tester.configServer().nodeRepository().list(DeploymentContext.systemTest.zone(),
- NodeFilter.all().applications(app.instanceId())).iterator().next();
- tester.clock().advance(InternalStepRunner.Timeouts.of(system()).noNodesDown().minus(Duration.ofSeconds(1)));
- tester.configServer().nodeRepository().putNodes(DeploymentContext.systemTest.zone(),
- Node.builder(systemTestNode)
- .serviceState(Node.ServiceState.allowedDown)
- .suspendedSince(tester.clock().instant())
- .build());
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installReal));
- assertEquals(unfinished, tester.jobs().last(app.instanceId(), DeploymentContext.stagingTest).get().stepStatuses().get(Step.installInitialReal));
-
- tester.clock().advance(Duration.ofSeconds(2));
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installReal));
- assertEquals(failed, tester.jobs().last(app.instanceId(), DeploymentContext.stagingTest).get().stepStatuses().get(Step.installInitialReal));
-
- tester.clock().advance(InternalStepRunner.Timeouts.of(system()).statelessNodesDown().minus(Duration.ofSeconds(3)));
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installReal));
-
- tester.clock().advance(Duration.ofSeconds(2));
- tester.runner().run();
- assertEquals(failed, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installReal));
- }
-
- @Test
- public void startingTestsFailsIfDeploymentExpires() {
- app.newRun(DeploymentContext.systemTest);
- tester.runner().run();
- tester.configServer().convergeServices(app.instanceId(), DeploymentContext.systemTest.zone());
- tester.runner().run();
- assertEquals(succeeded, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installReal));
- assertEquals(unfinished, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installTester));
-
- tester.applications().deactivate(app.instanceId(), DeploymentContext.systemTest.zone());
- tester.configServer().convergeServices(app.testerId().id(), DeploymentContext.systemTest.zone());
- tester.runner().run();
- assertEquals(succeeded, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installTester));
- assertEquals(failed, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.startTests));
- assertTrue(tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().hasEnded());
- assertTrue(tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().hasFailed());
- }
-
- @Test
- public void alternativeEndpointsAreDetected() {
- var systemTestZone = DeploymentContext.systemTest.zone();
- var stagingZone = DeploymentContext.stagingTest.zone();
- tester.controllerTester().zoneRegistry().exclusiveRoutingIn(ZoneApiMock.from(systemTestZone), ZoneApiMock.from(stagingZone));
- var applicationPackage = new ApplicationPackageBuilder()
- .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"))
- .upgradePolicy("default")
- .region("us-central-1")
- .parallel("us-west-1", "us-east-3")
- .build();
- app.submit(applicationPackage)
- .triggerJobs();
-
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installReal));
- assertEquals(unfinished, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installTester));
-
- app.flushDnsUpdates();
- tester.configServer().convergeServices(app.instanceId(), DeploymentContext.systemTest.zone());
- tester.configServer().convergeServices(app.testerId().id(), DeploymentContext.systemTest.zone());
- tester.runner().run();
- assertEquals(succeeded, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installReal));
- assertEquals(succeeded, tester.jobs().last(app.instanceId(), DeploymentContext.systemTest).get().stepStatuses().get(Step.installTester));
- }
-
- @Test
- public void noTestsThenErrorIsError() {
- RunId id = app.startSystemTestTests();
- Run run = tester.jobs().run(id);
- run = run.with(noTests, new LockedStep(() -> { }, Step.endTests));
- assertFalse(run.hasFailed());
- run = run.with(RunStatus.error, new LockedStep(() -> { }, Step.deactivateReal));
- assertTrue(run.hasFailed());
- assertEquals(RunStatus.error, run.status());
- }
-
- @Test
- public void noTestsThenSuccessIsNoTests() {
- RunId id = app.startSystemTestTests();
- tester.cloud().set(Status.NO_TESTS);
- tester.runner().run();
- assertEquals(succeeded, tester.jobs().run(id).stepStatuses().get(Step.endTests));
- Run run = tester.jobs().run(id);
- assertEquals(noTests, run.status());
- }
-
- @Test
- public void testsFailIfTesterRestarts() {
- RunId id = app.startSystemTestTests();
- tester.cloud().set(TesterCloud.Status.NOT_STARTED);
- tester.runner().run();
- assertEquals(failed, tester.jobs().run(id).stepStatuses().get(Step.endTests));
- }
-
- @Test
- public void testsFailIfTestsFailRemotely() {
- RunId id = app.startSystemTestTests();
- tester.cloud().add(new LogEntry(123, Instant.ofEpochMilli(321), error, "Failure!"));
- tester.cloud().set(TesterCloud.Status.FAILURE);
-
- long lastId = tester.jobs().details(id).get().lastId().getAsLong();
- tester.runner().run();
- assertTestLogEntries(id, Step.endTests,
- new LogEntry(lastId + 1, Instant.ofEpochMilli(321), error, "Failure!"),
- new LogEntry(lastId + 2, tester.clock().instant(), info, "Tests failed."));
- assertEquals(failed, tester.jobs().run(id).stepStatuses().get(Step.endTests));
- }
-
- @Test
- public void testsFailIfTestsErr() {
- RunId id = app.startSystemTestTests();
- tester.cloud().add(new LogEntry(0, Instant.ofEpochMilli(123), error, "Error!"));
- tester.cloud().set(TesterCloud.Status.ERROR);
-
- long lastId = tester.jobs().details(id).get().lastId().getAsLong();
- tester.runner().run();
- assertEquals(failed, tester.jobs().run(id).stepStatuses().get(Step.endTests));
- assertTestLogEntries(id, Step.endTests,
- new LogEntry(lastId + 1, Instant.ofEpochMilli(123), error, "Error!"),
- new LogEntry(lastId + 2, tester.clock().instant(), info, "Tester failed running its tests!"));
- }
-
- @Test
- public void testsSucceedWhenTheyDoRemotely() {
- RunId id = app.startSystemTestTests();
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.endTests));
- var testZone = DeploymentContext.systemTest.zone();
- Inspector configObject = SlimeUtils.jsonToSlime(tester.cloud().config()).get();
- assertEquals(app.instanceId().serializedForm(), configObject.field("application").asString());
- assertEquals(testZone.value(), configObject.field("zone").asString());
- assertEquals(system().value(), configObject.field("system").asString());
- assertEquals(1, configObject.field("zoneEndpoints").children());
- assertEquals(1, configObject.field("zoneEndpoints").field(testZone.value()).children());
-
- long lastId = tester.jobs().details(id).get().lastId().getAsLong();
- tester.cloud().add(new LogEntry(0, Instant.ofEpochMilli(123), info, "Ready!"));
- tester.runner().run();
- assertTestLogEntries(id, Step.endTests,
- new LogEntry(lastId + 1, Instant.ofEpochMilli(123), info, "Ready!"));
-
- tester.cloud().add(new LogEntry(1, Instant.ofEpochMilli(1234), info, "Steady!"));
- tester.runner().run();
- assertTestLogEntries(id, Step.endTests,
- new LogEntry(lastId + 1, Instant.ofEpochMilli(123), info, "Ready!"),
- new LogEntry(lastId + 2, Instant.ofEpochMilli(1234), info, "Steady!"));
-
- tester.cloud().add(new LogEntry(12, Instant.ofEpochMilli(12345), info, "Success!"));
- tester.cloud().set(TesterCloud.Status.SUCCESS);
- tester.runner().run();
- assertTestLogEntries(id, Step.endTests,
- new LogEntry(lastId + 1, Instant.ofEpochMilli(123), info, "Ready!"),
- new LogEntry(lastId + 2, Instant.ofEpochMilli(1234), info, "Steady!"),
- new LogEntry(lastId + 3, Instant.ofEpochMilli(12345), info, "Success!"),
- new LogEntry(lastId + 4, tester.clock().instant(), info, "Tests completed successfully."));
- assertEquals(succeeded, tester.jobs().run(id).stepStatuses().get(Step.endTests));
- }
-
- @Test
- public void testCanBeReset() {
- RunId id = app.startSystemTestTests();
- tester.cloud().add(new LogEntry(0, Instant.ofEpochMilli(123), info, "Not enough data!"));
- tester.cloud().set(TesterCloud.Status.INCONCLUSIVE);
- tester.cloud().testReport(TestReport.fromJson("{\"foo\":1}"));
-
- long lastId1 = tester.jobs().details(id).get().lastId().getAsLong();
- Instant instant1 = tester.clock().instant();
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.endTests));
- assertEquals(running, tester.jobs().run(id).status());
- tester.cloud().clearLog();
-
- // Test sleeps for a while.
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployTester));
- Instant nextAttemptAt = tester.clock().instant().plusSeconds(1800);
- tester.clock().advance(Duration.ofSeconds(1799));
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployTester));
-
- tester.clock().advance(JobRunner.jobTimeout);
- var testZone = DeploymentContext.systemTest.zone();
- tester.runner().run();
- app.flushDnsUpdates();
- tester.configServer().convergeServices(app.instanceId(), testZone);
- tester.configServer().convergeServices(app.testerId().id(), testZone);
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.endTests));
- assertTrue(tester.jobs().run(id).steps().get(Step.endTests).startTime().isPresent());
-
- tester.cloud().set(TesterCloud.Status.SUCCESS);
- tester.cloud().testReport(TestReport.fromJson("{\"bar\":2}"));
- long lastId2 = tester.jobs().details(id).get().lastId().getAsLong();
- tester.runner().run();
- assertEquals(success, tester.jobs().run(id).status());
-
- assertTestLogEntries(id, Step.endTests,
- new LogEntry(lastId1 + 1, Instant.ofEpochMilli(123), info, "Not enough data!"),
- new LogEntry(lastId1 + 2, instant1, info, "Tests were inconclusive, and will run again at " + nextAttemptAt + "."),
- new LogEntry(lastId1 + 15, instant1, info, "### Run will reset, and start over at " + nextAttemptAt),
- new LogEntry(lastId1 + 16, instant1, info, ""),
- new LogEntry(lastId2 + 1, tester.clock().instant(), info, "Tests completed successfully."));
-
- assertEquals("[{\"foo\":1},{\"bar\":2}]", tester.jobs().getTestReports(id).get());
- }
-
- @Test
- public void deployToDev() {
- ZoneId zone = DeploymentContext.devUsEast1.zone();
- tester.jobs().deploy(app.instanceId(), DeploymentContext.devUsEast1, Optional.empty(), applicationPackage());
- tester.runner().run();
- RunId id = tester.jobs().last(app.instanceId(), DeploymentContext.devUsEast1).get().id();
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.installReal));
-
- Version version = new Version("7.8.9");
- tester.controllerTester().upgradeSystem(version);
- Future<?> concurrentDeployment = Executors.newSingleThreadExecutor().submit(() -> {
- tester.jobs().deploy(app.instanceId(), DeploymentContext.devUsEast1, Optional.of(version), applicationPackage());
- });
- while ( ! concurrentDeployment.isDone())
- tester.runner().run();
- assertEquals(id.number() + 1, tester.jobs().last(app.instanceId(), DeploymentContext.devUsEast1).get().id().number());
-
- ApplicationPackage otherPackage = new ApplicationPackageBuilder().region("us-central-1").build();
- tester.jobs().deploy(app.instanceId(), DeploymentContext.perfUsEast3, Optional.empty(), otherPackage);
-
- tester.runner().run(); // Job run order determined by JobType enum order per application.
- tester.configServer().convergeServices(app.instanceId(), zone);
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.installReal));
-
- tester.configServer().setVersion(version, app.instanceId(), zone);
- tester.runner().run();
- assertEquals(1, tester.jobs().active().size());
- assertEquals(version, tester.instance(app.instanceId()).deployments().get(zone).version());
- }
-
- @Test
- public void notificationIsSent() {
- app.submit().failDeployment(DeploymentContext.systemTest);
- MockMailer mailer = tester.controllerTester().serviceRegistry().mailer();
- assertEquals(1, mailer.inbox("a@b").size());
- assertEquals("Vespa application tenant.application: System test failing due to system error",
- mailer.inbox("a@b").get(0).subject());
- assertEquals(1, mailer.inbox("b@a").size());
- assertEquals("Vespa application tenant.application: System test failing due to system error",
- mailer.inbox("b@a").get(0).subject());
-
- // Re-run failing causes no additional email to be sent.
- app.failDeployment(DeploymentContext.systemTest);
- assertEquals(1, mailer.inbox("a@b").size());
- assertEquals(1, mailer.inbox("b@a").size());
-
- // Failure with new package causes new email to be sent.
- app.submit().failDeployment(DeploymentContext.systemTest);
- assertEquals(2, mailer.inbox("a@b").size());
- assertEquals(2, mailer.inbox("b@a").size());
- }
-
- @Test
- public void vespaLogIsCopied() {
- // Tests fail. We should get logs. This fails too, on the first attempt.
- tester.controllerTester().computeVersionStatus();
- RunId id = app.startSystemTestTests();
- tester.cloud().set(TesterCloud.Status.ERROR);
- tester.configServer().setLogStream(() -> { throw new ConfigServerException(ConfigServerException.ErrorCode.NOT_FOUND, "404", "context"); });
- long lastId = tester.jobs().details(id).get().lastId().getAsLong();
- tester.runner().run();
- assertEquals(failed, tester.jobs().run(id).stepStatuses().get(Step.endTests));
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.copyVespaLogs));
- assertTestLogEntries(id, Step.copyVespaLogs,
- new LogEntry(lastId + 2, tester.clock().instant(), info,
- "Found no logs, but will retry"));
-
- // Config servers now provide the log, and we get it.
- tester.configServer().setLogStream(() -> vespaLog(tester.clock().instant()));
- tester.runner().run();
- assertEquals(failed, tester.jobs().run(id).stepStatuses().get(Step.endTests));
- assertTestLogEntries(id, Step.copyVespaLogs,
- new LogEntry(lastId + 2, tester.clock().instant(), info,
- "Found no logs, but will retry"),
- new LogEntry(lastId + 3, tester.clock().instant().minusSeconds(4), info,
- "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" +
- "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"),
- new LogEntry(lastId + 4, tester.clock().instant().minusSeconds(4), info,
- "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" +
- "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"),
- new LogEntry(lastId + 5, tester.clock().instant().minusSeconds(4), info,
- "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" +
- "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"),
- new LogEntry(lastId + 6, tester.clock().instant().minusSeconds(4), info,
- "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" +
- "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"),
- new LogEntry(lastId + 7, tester.clock().instant().minusSeconds(3), info,
- "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" +
- "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"),
- new LogEntry(lastId + 8, tester.clock().instant().minusSeconds(3), warning,
- "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstderr\n" +
- "java.lang.NullPointerException\n\tat org.apache.felix.framework.BundleRevisionImpl.calculateContentPath(BundleRevisionImpl.java:438)\n\tat org.apache.felix.framework.BundleRevisionImpl.initializeContentPath(BundleRevisionImpl.java:371)"),
- new LogEntry(lastId + 9, tester.clock().instant().minusSeconds(3), info,
- "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\n" +
- "ERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"),
- new LogEntry(lastId + 10, tester.clock().instant().minusSeconds(3), warning,
- "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstderr\n" +
- "java.lang.NullPointerException\n\tat org.apache.felix.framework.BundleRevisionImpl.calculateContentPath(BundleRevisionImpl.java:438)\n\tat org.apache.felix.framework.BundleRevisionImpl.initializeContentPath(BundleRevisionImpl.java:371)"));
- }
-
- @Test
- public void realDeploymentRequiresForTesterCert() {
- List<ZoneApiMock> zones = List.of(ZoneApiMock.fromId("test.aws-us-east-1c"),
- ZoneApiMock.fromId("staging.aws-us-east-1c"),
- ZoneApiMock.fromId("prod.aws-us-east-1c"));
- ControllerTester wrapped = new ControllerTester(SystemName.Public);
- wrapped.zoneRegistry()
- .setZones(zones)
- .setRoutingMethod(zones, RoutingMethod.exclusive);
- tester = new DeploymentTester(wrapped);
- tester.configServer().bootstrap(tester.controllerTester().zoneRegistry().zones().all().ids(), SystemApplication.values());
- app = tester.newDeploymentContext();
- RunId id = app.newRun(DeploymentContext.systemTest);
- tester.configServer().throwOnPrepare(instanceId -> {
- if (instanceId.instance().isTester())
- throw new ConfigServerException(ConfigServerException.ErrorCode.PARENT_HOST_NOT_READY, "provisioning", "deploy tester");
- });
- tester.runner().run();
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployTester));
- assertEquals(unfinished, tester.jobs().run(id).stepStatuses().get(Step.deployReal));
-
- List<X509Certificate> oldTesterCert = List.of(tester.jobs().run(id).testerCertificate().get());
-
- assertEquals(oldTesterCert, tester.configServer().additionalCertificates(app.deploymentIdIn(id.type().zone())));
-
- tester.configServer().throwOnNextPrepare(null);
- tester.clock().advance(Duration.ofSeconds(450));
- tester.runner().run();
- assertEquals(succeeded, tester.jobs().run(id).stepStatuses().get(Step.deployTester));
- assertEquals(succeeded, tester.jobs().run(id).stepStatuses().get(Step.deployReal));
-
- List<X509Certificate> newTesterCert = List.of(tester.jobs().run(id).testerCertificate().get());
- assertEquals(newTesterCert, tester.configServer().additionalCertificates(app.deploymentIdIn(id.type().zone())));
-
- assertNotEquals(oldTesterCert, newTesterCert);
- }
-
- @Test
- public void certificateTimeoutAbortsJob() {
- tester = new DeploymentTester(new ControllerTester(SystemName.Public));
- app = tester.newDeploymentContext();
- RunId id = app.startSystemTestTests();
-
-
- assertEquals(List.of(tester.jobs().run(id).testerCertificate().get()), tester.configServer().additionalCertificates(app.deploymentIdIn(id.type().zone())));
-
- tester.clock().advance(InternalStepRunner.Timeouts.of(system()).testerCertificate().plus(Duration.ofSeconds(1)));
- tester.runner().run();
- assertEquals(RunStatus.error, tester.jobs().run(id).status());
- }
-
-
- @Test
- public void quotaExceededAbortsJob() {
- RuntimeException exception = new ConfigServerException(ConfigServerException.ErrorCode.QUOTA_EXCEEDED,
- "Quota exceeded",
- "deploy failure");
- tester.configServer().throwOnNextPrepare(exception);
- tester.jobs().deploy(app.instanceId(), DeploymentContext.devUsEast1, Optional.empty(), applicationPackage());
- assertEquals(failed, tester.jobs().last(app.instanceId(), DeploymentContext.devUsEast1).get().stepStatuses().get(Step.deployReal));
- assertEquals(quotaExceeded, tester.jobs().last(app.instanceId(), DeploymentContext.devUsEast1).get().status());
- }
-
- private void assertTestLogEntries(RunId id, Step step, LogEntry... entries) {
- assertEquals(List.of(entries), tester.jobs().details(id).get().get(step));
- }
-
- private static String vespaLog(Instant now) {
- return "-1\t17480180-v6-3.ostk.bm2.prod.ne1.yahoo.com\t5549/832\tcontainer\tContainer.com.yahoo.container.jdisc.ConfiguredApplication\tinfo\tSwitching to the latest deployed set of configurations and components. Application switch number: 2\n" +
- (now.getEpochSecond() - 4) + "." + now.getNano() / 1000 + "\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n" +
- (now.getEpochSecond() - 4) + "." + now.getNano() / 1000 + "\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n" +
- (now.getEpochSecond() - 3) + "." + now.getNano() / 1000 + "\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n" +
- (now.getEpochSecond() - 3) + "." + now.getNano() / 1000 + "\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstderr\twarning\tjava.lang.NullPointerException\\n\\tat org.apache.felix.framework.BundleRevisionImpl.calculateContentPath(BundleRevisionImpl.java:438)\\n\\tat org.apache.felix.framework.BundleRevisionImpl.initializeContentPath(BundleRevisionImpl.java:371)";
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/QuotaUsageTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/QuotaUsageTest.java
deleted file mode 100644
index 1e918c27231..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/QuotaUsageTest.java
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author ogronnesby
- */
-public class QuotaUsageTest {
-
- @Test
- void testQuotaUsageIsPersisted() {
- var tester = new DeploymentTester();
- var context = tester.newDeploymentContext().submit().deploy();
- assertEquals(1.304, context.deployment(ZoneId.from("prod.us-west-1")).quota().rate(), 0.01);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java
deleted file mode 100644
index 0864ebb1154..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.time.Instant;
-import java.util.List;
-import java.util.Map;
-
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTester.instanceId;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author jonmv
- */
-public class TestConfigSerializerTest {
-
- @Test
- void testConfig() throws IOException {
- ZoneId zone = DeploymentContext.systemTest.zone();
- byte[] json = new TestConfigSerializer(SystemName.PublicCd)
- .configJson(instanceId,
- DeploymentContext.systemTest,
- true,
- Version.fromString("1.2.3"),
- RevisionId.forProduction(321),
- Instant.ofEpochMilli(222),
- Map.of(zone, List.of(Endpoint.of(ApplicationId.defaultId())
- .target(EndpointId.of("ai"), ClusterSpec.Id.from("qrs"),
- List.of(new DeploymentId(ApplicationId.defaultId(),
- ZoneId.defaultId())))
- .on(Endpoint.Port.tls())
- .in(SystemName.main))),
- Map.of(zone, List.of("facts")));
- byte[] expected = Files.readAllBytes(Paths.get("src/test/resources/testConfig.json"));
- assertEquals(new String(SlimeUtils.toJsonBytes(SlimeUtils.jsonToSlime(expected))),
- new String(json));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ZipBuilderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ZipBuilderTest.java
deleted file mode 100644
index bb389bb91c2..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/ZipBuilderTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.deployment;
-
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author freva
- */
-public class ZipBuilderTest {
-
- @Test
- void test() {
- Map<String, String> expected = new HashMap<>();
- expected.put("dir/myfile", "my content");
- expected.put("rootfile", "this is root");
- expected.put("dir/newfile", "new file");
- expected.put("dir/dir2/file", "nested file");
-
- try (ZipBuilder zipBuilder1 = new ZipBuilder(100);
- ZipBuilder zipBuilder2 = new ZipBuilder(1000)) {
-
- // Add the entries to both zip builders one by one
- Iterator<Map.Entry<String, String>> entries = expected.entrySet().iterator();
- for (int i = 0; entries.hasNext(); i++) {
- Map.Entry<String, String> entry = entries.next();
- (i % 2 == 0 ? zipBuilder1 : zipBuilder2)
- .add(entry.getKey(), entry.getValue().getBytes(StandardCharsets.UTF_8));
- }
-
- // Add the zipped data from zip1 to zip2
- zipBuilder2.add(zipBuilder1.toByteArray(), __ -> true);
-
- Map<String, String> actual = unzipToMap(zipBuilder2.toByteArray());
- assertEquals(expected, actual);
- }
- }
-
- Map<String, String> unzipToMap(byte[] zippedContent) {
- Map<String, String> contents = new HashMap<>();
- try (ZipInputStream zin = new ZipInputStream(new ByteArrayInputStream(zippedContent))) {
- for (ZipEntry entry = zin.getNextEntry(); entry != null; entry = zin.getNextEntry()) {
- if (entry.isDirectory()) continue;
- contents.put(entry.getName(), new String(zin.readAllBytes(), StandardCharsets.UTF_8));
- }
- } catch (IOException e) {
- throw new UncheckedIOException("Failed to read zipped content", e);
- }
- return contents;
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/dns/NameServiceQueueTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/dns/NameServiceQueueTest.java
deleted file mode 100644
index 36619d4ca93..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/dns/NameServiceQueueTest.java
+++ /dev/null
@@ -1,211 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.dns;
-
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.LatencyAliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record.Type;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue.Priority;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Consumer;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-public class NameServiceQueueTest {
-
- @Test
- void test_queue() {
- var nameService = new MemoryNameService();
- var r1 = new Record(Record.Type.CNAME, RecordName.from("cname.vespa.oath.cloud"), RecordData.from("example.com"));
- var r2 = new Record(Record.Type.TXT, RecordName.from("txt.example.com"), RecordData.from("text"));
- var r3 = List.of(new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"),
- new LatencyAliasTarget(HostName.of("alias1"),
- "dns-zone-01",
- ZoneId.from("prod", "us-north-1")).pack()),
- new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"),
- new LatencyAliasTarget(HostName.of("alias2"),
- "dns-zone-02",
- ZoneId.from("prod", "us-north-2")).pack()));
- var o0 = Optional.<TenantAndApplicationId>empty();
- var o1 = Optional.of(TenantAndApplicationId.from("t", "a"));
- var o2 = Optional.of(TenantAndApplicationId.from("t", "b"));
- var req1 = new CreateRecord(o0, r1);
- var req2 = new CreateRecords(o1, List.of(r2));
- var req3 = new CreateRecords(o2, r3);
- var req4 = new RemoveRecords(o0, r3.get(0).type(), r3.get(0).name());
- var req5 = new RemoveRecords(o1, r2.type(), r2.name(), r2.data());
- var req6 = new RemoveRecords(o2, Record.Type.CNAME, r1.name(), r1.data());
-
- // Add requests with different priorities and dispatch first one
- var queue = NameServiceQueue.EMPTY.with(req2).with(req1, Priority.high);
- assertEquals(2, queue.requests().size());
- queue = queue.dispatchTo(nameService, 1);
- assertEquals(r1, nameService.findRecords(r1.type(), r1.name()).get(0));
- assertEquals(1, queue.requests().size());
- assertEquals(req2, queue.requests().iterator().next());
-
- // Dispatch remaining requests
- queue = queue.dispatchTo(nameService, 10);
- assertTrue(queue.requests().isEmpty());
- assertEquals(r2, nameService.findRecords(r2.type(), r2.name()).get(0));
-
- // Dispatch from empty queue
- assertSame(queue, queue.dispatchTo(nameService, 10));
-
- // Dispatch create alias
- queue = queue.with(req3).dispatchTo(nameService, 1);
- assertEquals(r3, nameService.findRecords(Record.Type.ALIAS, r3.get(0).name()));
-
- // Dispatch removals
- queue = queue.with(req4).with(req5).dispatchTo(nameService, 2);
- assertTrue(nameService.findRecords(r2.type(), r2.name()).isEmpty(), "Removed " + r2);
- assertTrue(nameService.findRecords(Record.Type.ALIAS, r3.get(0).name()).isEmpty(), "Removed " + r3);
-
- // Dispatch removals by data
- queue = queue.with(req6).dispatchTo(nameService, 1);
- assertTrue(queue.requests().isEmpty());
- assertTrue(nameService.findRecords(Record.Type.CNAME, r1.name()).isEmpty(), "Removed " + r1);
-
- // Keep n last requests
- queue = queue.with(req1).with(req2).with(req3).with(req4).with(req6)
- .last(2);
- assertEquals(List.of(req4, req6), List.copyOf(queue.requests()));
- assertSame(queue, queue.last(2));
- assertSame(queue, queue.last(10));
- assertTrue(queue.last(0).requests().isEmpty());
-
- // Keep n first requests
- queue = NameServiceQueue.EMPTY.with(req1).with(req2).with(req3).with(req4).with(req6)
- .first(3);
- assertEquals(List.of(req1, req2, req3), List.copyOf(queue.requests()));
- assertSame(queue, queue.first(3));
- assertSame(queue, queue.first(10));
- assertTrue(queue.first(0).requests().isEmpty());
-
- // Remove some requests
- queue = new NameServiceQueue(List.of(req1, req2, req2, req3)).without(new NameServiceQueue(List.of(req1, req2)));
- assertEquals(List.of(req2, req3), queue.requests());
- }
-
- @Test
- void test_failing_requests() {
- Deque<Consumer<RecordName>> expectations = new ArrayDeque<>();
- var nameService = new NameService() {
- @Override public Record createRecord(Type type, RecordName name, RecordData data) {
- var expectation = expectations.poll();
- assertNotNull(expectation, "unexpected dispatch; add more expectations, or fix the bug!");
- expectation.accept(name);
- return null;
- }
- @Override public List<Record> createAlias(RecordName name, Set<AliasTarget> targets) { throw new UnsupportedOperationException(); }
- @Override public List<Record> createDirect(RecordName name, Set<DirectTarget> targets) { throw new UnsupportedOperationException(); }
- @Override public List<Record> createTxtRecords(RecordName name, List<RecordData> txtRecords) { throw new UnsupportedOperationException(); }
- @Override public List<Record> findRecords(Type type, RecordName name) { return List.of(); }
- @Override public void updateRecord(Record record, RecordData newData) { throw new UnsupportedOperationException(); }
- @Override public void removeRecords(List<Record> record) { throw new UnsupportedOperationException(); }
- };
-
- var owner0 = Optional.<TenantAndApplicationId>empty();
- var owner1 = Optional.of(TenantAndApplicationId.from("t", "a"));
- var owner2 = Optional.of(TenantAndApplicationId.from("t", "b"));
-
- var rec1 = new Record(Type.A, RecordName.from("one"), RecordData.from("data"));
- var rec2 = new Record(Type.A, RecordName.from("two"), RecordData.from("data"));
- var rec3 = new Record(Type.A, RecordName.from("three"), RecordData.from("data"));
- var rec4 = new Record(Type.A, RecordName.from("four"), RecordData.from("data"));
-
- var req1 = new CreateRecord(owner0, rec1);
- var req2 = new CreateRecord(owner1, rec2);
- var req3 = new CreateRecord(owner1, rec3);
- var req4 = new CreateRecord(owner2, rec4);
- var req5 = new CreateRecord(owner1, rec4);
- var req6 = new CreateRecord(owner0, rec1);
- var req7 = new CreateRecord(owner1, rec4);
- var req8 = new CreateRecord(owner0, rec2);
-
- var base = List.<NameServiceRequest>of(req1, req2, req3, req4, req5, req6, req7, req8);
-
- // failing operator request, nothing happens repeatedly
- RuntimeException exception = new RuntimeException();
- for (int i = 0; i < 4; i++) expectations.add(name -> { assertEquals(rec1.name(), name); throw exception; });
- assertEquals(base,
- new NameServiceQueue(base).dispatchTo(nameService, 4).requests());
- assertEquals(0, expectations.size());
-
- // operator request OK, owner1 fails first request, owner2 is moved first in the queue, but not yet dispatched
- expectations.add(name -> { assertEquals(rec1.name(), name); });
- expectations.add(name -> { assertEquals(rec2.name(), name); throw exception; });
- assertEquals(List.of(req4, req2, req3, req5, req6, req7, req8),
- new NameServiceQueue(base).dispatchTo(nameService, 2).requests());
- assertEquals(0, expectations.size());
-
- // operator request OK, owner1 fails first request, then owner2 gets to run one request, then owner1's first request is attempted repeatedly
- expectations.add(name -> { assertEquals(rec1.name(), name); });
- expectations.add(name -> { assertEquals(rec2.name(), name); throw exception; });
- expectations.add(name -> { assertEquals(rec4.name(), name); });
- expectations.add(name -> { assertEquals(rec2.name(), name); throw exception; });
- expectations.add(name -> { assertEquals(rec2.name(), name); throw exception; });
- assertEquals(List.of(req2, req3, req5, req6, req7, req8),
- new NameServiceQueue(base).dispatchTo(nameService, 5).requests());
- assertEquals(0, expectations.size());
-
- expectations.add(name -> { assertEquals(rec1.name(), name); throw exception; }); // operator fails
- expectations.add(name -> { assertEquals(rec1.name(), name); }); // operator succeeds
- expectations.add(name -> { assertEquals(rec2.name(), name); }); // owner1 succeeds
- expectations.add(name -> { assertEquals(rec3.name(), name); throw exception; }); // owner1 fails
- expectations.add(name -> { assertEquals(rec4.name(), name); throw exception; }); // owner2 fails
- expectations.add(name -> { assertEquals(rec3.name(), name); throw exception; }); // owner1 fails
- expectations.add(name -> { assertEquals(rec4.name(), name); }); // owner2 succeeds
- expectations.add(name -> { assertEquals(rec3.name(), name); }); // owner1 succeeds
- expectations.add(name -> { assertEquals(rec4.name(), name); }); // owner1 succeeds
- expectations.add(name -> { assertEquals(rec1.name(), name); throw exception; }); // operator fails
- expectations.add(name -> { assertEquals(rec1.name(), name); }); // operator succeeds
- expectations.add(name -> { assertEquals(rec4.name(), name); throw exception; }); // owner1 fails
- expectations.add(name -> { assertEquals(rec4.name(), name); }); // owner1 succeeds
- expectations.add(name -> { assertEquals(rec2.name(), name); }); // operator succeeds
- assertEquals(List.of(),
- new NameServiceQueue(base).dispatchTo(nameService, 100).requests());
- assertEquals(0, expectations.size());
-
- // Finally, let the queue fill past its capacity, and see that failed requests are simply dropped instead.
- expectations.add(name -> { assertEquals(rec1.name(), name); throw exception; });
- expectations.add(name -> { assertEquals(rec2.name(), name); });
- expectations.add(name -> { assertEquals(rec1.name(), name); throw exception; });
- expectations.add(name -> { assertEquals(rec2.name(), name); });
- var full = new LinkedList<NameServiceRequest>();
- for (int i = 0; i < NameServiceQueue.QUEUE_CAPACITY; i++) {
- full.add(req1);
- full.add(req2);
- }
- assertEquals(full.subList(4, full.size()),
- new NameServiceQueue(full).dispatchTo(nameService, 4).requests());
-
- // However, if the queue is even fuller, at the end of a dispatch run, the oldest requests are discarded too.
- full.add(req3);
- full.add(req4);
- assertEquals(full.subList(2, full.size()),
- new NameServiceQueue(full).dispatchTo(nameService, 0).requests());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java
deleted file mode 100644
index a064cbb82d2..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ApplicationStoreMock.java
+++ /dev/null
@@ -1,158 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.NotExistsException;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationStore;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterId;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.time.Instant;
-import java.util.Map;
-import java.util.NavigableMap;
-import java.util.Optional;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentSkipListMap;
-
-import static java.util.Objects.requireNonNull;
-
-/**
- * Threadsafe.
- *
- * @author jonmv
- */
-public class ApplicationStoreMock implements ApplicationStore {
-
- private static final byte[] tombstone = new byte[0];
-
- private final Map<ApplicationId, Map<RevisionId, byte[]>> store = new ConcurrentHashMap<>();
- private final Map<DeploymentId, byte[]> devStore = new ConcurrentHashMap<>();
- private final Map<ApplicationId, Map<Long, byte[]>> diffs = new ConcurrentHashMap<>();
- private final Map<DeploymentId, Map<Long, byte[]>> devDiffs = new ConcurrentHashMap<>();
- private final Map<ApplicationId, NavigableMap<Instant, byte[]>> meta = new ConcurrentHashMap<>();
- private final Map<DeploymentId, NavigableMap<Instant, byte[]>> metaManual = new ConcurrentHashMap<>();
-
- private static ApplicationId appId(TenantName tenant, ApplicationName application) {
- return ApplicationId.from(tenant, application, InstanceName.defaultName());
- }
-
- private static ApplicationId testerId(TenantName tenant, ApplicationName application) {
- return TesterId.of(appId(tenant, application)).id();
- }
-
- @Override
- public InputStream stream(DeploymentId deploymentId, RevisionId revisionId) {
- if ( ! revisionId.isProduction())
- return new ByteArrayInputStream(devStore.get(deploymentId));
-
- TenantAndApplicationId tenantAndApplicationId = TenantAndApplicationId.from(deploymentId.applicationId());
- byte[] bytes = store.get(appId(tenantAndApplicationId.tenant(), tenantAndApplicationId.application())).get(revisionId);
- if (bytes == null) throw new NotExistsException("No " + revisionId + " found for " + tenantAndApplicationId);
- return new ByteArrayInputStream(bytes);
- }
-
- @Override
- public Optional<byte[]> getDiff(TenantName tenantName, ApplicationName applicationName, long buildNumber) {
- return Optional.ofNullable(diffs.get(appId(tenantName, applicationName))).map(map -> map.get(buildNumber));
- }
-
- @Override
- public void pruneDiffs(TenantName tenantName, ApplicationName applicationName, long beforeBuildNumber) {
- Optional.ofNullable(diffs.get(appId(tenantName, applicationName)))
- .ifPresent(map -> map.keySet().removeIf(buildNumber -> buildNumber < beforeBuildNumber));
- }
-
- @Override
- public Optional<byte[]> find(TenantName tenant, ApplicationName application, long buildNumber) {
- return store.getOrDefault(appId(tenant, application), Map.of()).entrySet().stream()
- .filter(kv -> kv.getKey().number() == buildNumber)
- .map(Map.Entry::getValue)
- .findFirst();
- }
-
- @Override
- public void put(TenantName tenant, ApplicationName application, RevisionId revision, byte[] bytes, byte[] tests, byte[] diff) {
- store.computeIfAbsent(appId(tenant, application), __ -> new ConcurrentHashMap<>()).put(revision, bytes);
- store.computeIfAbsent(testerId(tenant, application), key -> new ConcurrentHashMap<>()) .put(revision, tests);
- diffs.computeIfAbsent(appId(tenant, application), __ -> new ConcurrentHashMap<>()).put(revision.number(), diff);
- }
-
- @Override
- public void prune(TenantName tenant, ApplicationName application, RevisionId oldestToRetain) {
- store.getOrDefault(appId(tenant, application), Map.of()).keySet().removeIf(version -> version.compareTo(oldestToRetain) < 0);
- store.getOrDefault(testerId(tenant, application), Map.of()).keySet().removeIf(version -> version.compareTo(oldestToRetain) < 0);
- }
-
- @Override
- public void removeAll(TenantName tenant, ApplicationName application) {
- store.remove(appId(tenant, application));
- store.remove(testerId(tenant, application));
- }
-
- @Override
- public InputStream streamTester(TenantName tenant, ApplicationName application, RevisionId revision) {
- return new ByteArrayInputStream(store.get(testerId(tenant, application)).get(revision));
- }
-
-
- @Override
- public Optional<byte[]> getDevDiff(DeploymentId deploymentId, long buildNumber) {
- return Optional.ofNullable(devDiffs.get(deploymentId)).map(map -> map.get(buildNumber));
- }
-
- @Override
- public void pruneDevDiffs(DeploymentId deploymentId, long beforeBuildNumber) {
- Optional.ofNullable(devDiffs.get(deploymentId))
- .ifPresent(map -> map.keySet().removeIf(buildNumber -> buildNumber < beforeBuildNumber));
- }
-
- @Override
- public void putDev(DeploymentId deploymentId, RevisionId revision, byte[] applicationPackage, byte[] diff) {
- devStore.put(deploymentId, applicationPackage);
- devDiffs.computeIfAbsent(deploymentId, __ -> new ConcurrentHashMap<>()).put(revision.number(), diff);
- }
-
- @Override
- public void putMeta(TenantName tenant, ApplicationName application, Instant now, byte[] metaZip) {
- meta.putIfAbsent(appId(tenant, application), new ConcurrentSkipListMap<>());
- meta.get(appId(tenant, application)).put(now, metaZip);
- }
-
- @Override
- public void putMetaTombstone(TenantName tenant, ApplicationName application, Instant now) {
- putMeta(tenant, application, now, tombstone);
- }
-
- @Override
- public void putMeta(DeploymentId id, Instant now, byte[] metaZip) {
- metaManual.computeIfAbsent(id, __ -> new ConcurrentSkipListMap<>()).put(now, metaZip);
- }
-
- @Override
- public void putMetaTombstone(DeploymentId id, Instant now) {
- putMeta(id, now, tombstone);
- }
-
- @Override
- public void pruneMeta(Instant oldest) {
- for (ApplicationId id : meta.keySet()) {
- Instant activeAtOldest = meta.get(id).lowerKey(oldest);
- if (activeAtOldest != null)
- meta.get(id).headMap(activeAtOldest).clear();
- if (meta.get(id).lastKey().isBefore(oldest) && meta.get(id).lastEntry().getValue() == tombstone)
- meta.remove(id);
- }
- }
-
- public NavigableMap<Instant, byte[]> getMeta(ApplicationId id) { return meta.get(id); }
-
- public NavigableMap<Instant, byte[]> getMeta(DeploymentId id) { return metaManual.get(id); }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ArtifactRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ArtifactRegistryMock.java
deleted file mode 100644
index 3c9e9679210..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ArtifactRegistryMock.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.yahoo.vespa.hosted.controller.api.integration.artifact.Artifact;
-import com.yahoo.vespa.hosted.controller.api.integration.artifact.ArtifactRegistry;
-
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * @author mpolden
- */
-public class ArtifactRegistryMock implements ArtifactRegistry {
-
- private static final Comparator<Artifact> comparator = Comparator.comparing(Artifact::registry)
- .thenComparing(Artifact::repository)
- .thenComparing(Artifact::version);
-
- private final Map<String, Artifact> images = new HashMap<>();
-
- public ArtifactRegistryMock add(Artifact image) {
- if (images.containsKey(image.id())) throw new IllegalArgumentException("Image with ID '" + image.id() + "' already exists");
- images.put(image.id(), image);
- return this;
- }
-
- @Override
- public void deleteAll(List<Artifact> artifacts) {
- artifacts.forEach(image -> this.images.remove(image.id()));
- }
-
- @Override
- public List<Artifact> list() {
- return images.values().stream().sorted(comparator).toList();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ArtifactRepositoryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ArtifactRepositoryMock.java
deleted file mode 100644
index e6915b0126e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ArtifactRepositoryMock.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ArtifactRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.OsRelease;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/**
- * @author mpolden
- */
-public class ArtifactRepositoryMock extends AbstractComponent implements ArtifactRepository {
-
- private final Map<String, OsRelease> releases = new HashMap<>();
-
- @Override
- public byte[] getSystemApplicationPackage(ApplicationId application, ZoneId zone, Version version) {
- return new byte[0];
- }
-
- @Override
- public OsRelease osRelease(int major, OsRelease.Tag tag) {
- OsRelease release = releases.get(key(major, tag));
- if (release == null) throw new IllegalArgumentException("No version set for major " + major + " with tag " + tag);
- return release;
- }
-
- public void addRelease(OsRelease osRelease) {
- releases.put(key(osRelease.version().getMajor(), osRelease.tag()), osRelease);
- }
-
- private static String key(int major, OsRelease.Tag tag) {
- return major + "@" + tag.name();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/AthenzFilterMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/AthenzFilterMock.java
deleted file mode 100644
index b781d8bb342..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/AthenzFilterMock.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.yahoo.jdisc.Response;
-import com.yahoo.jdisc.handler.FastContentWriter;
-import com.yahoo.jdisc.handler.ResponseDispatch;
-import com.yahoo.jdisc.handler.ResponseHandler;
-import com.yahoo.jdisc.http.HttpResponse;
-import com.yahoo.jdisc.http.filter.DiscFilterRequest;
-import com.yahoo.jdisc.http.filter.SecurityRequestFilter;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.utils.AthenzIdentities;
-import com.yahoo.yolean.chain.Before;
-
-import java.util.Optional;
-
-/**
- * @author bjorncs
- */
-@Before("ControllerAuthorizationFilter")
-public class AthenzFilterMock implements SecurityRequestFilter {
-
- public static final String IDENTITY_HEADER_NAME = "Athenz-Identity";
- public static final String OKTA_IDENTITY_TOKEN_HEADER_NAME = "Okta-Identity-Token";
- public static final String OKTA_ACCESS_TOKEN_HEADER_NAME = "Okta-Access-Token";
-
- private static final ObjectMapper mapper = new ObjectMapper();
-
- @Override
- public void filter(DiscFilterRequest request, ResponseHandler handler) {
- String identityName = request.getHeader(IDENTITY_HEADER_NAME);
- if (identityName == null) {
- Response response = new Response(HttpResponse.Status.UNAUTHORIZED);
- response.headers().put("Content-Type", "application/json");
- ObjectNode errorMessage = mapper.createObjectNode();
- errorMessage.put("message", "Not authenticated");
- try (FastContentWriter writer = ResponseDispatch.newInstance(response).connectFastWriter(handler)) {
- writer.write(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(errorMessage));
- } catch (JsonProcessingException e) {
- throw new RuntimeException(e);
- }
- } else {
- AthenzIdentity identity = AthenzIdentities.from(identityName);
- AthenzPrincipal principal = new AthenzPrincipal(identity);
- request.setUserPrincipal(principal);
- }
- Optional.ofNullable(request.getHeader(OKTA_IDENTITY_TOKEN_HEADER_NAME))
- .ifPresent(header -> request.setAttribute("okta.identity-token", header));
- Optional.ofNullable(request.getHeader(OKTA_ACCESS_TOKEN_HEADER_NAME))
- .ifPresent(header -> request.setAttribute("okta.access-token", header));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
deleted file mode 100644
index 5995b3eaac6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerMock.java
+++ /dev/null
@@ -1,634 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import ai.vespa.http.DomainName;
-import ai.vespa.http.HttpURL.Path;
-import ai.vespa.http.HttpURL.Query;
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.ClusterResources;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.DockerImage;
-import com.yahoo.config.provision.EndpointsChecker.Availability;
-import com.yahoo.config.provision.EndpointsChecker.Endpoint;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.IntRange;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.ZoneEndpoint.AllowedUrn;
-import com.yahoo.config.provision.ZoneEndpoint.AccessType;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.DeploymentData;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.EndpointStatus;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.SearchNodeMetrics;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing.Status;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServer;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerEndpoint;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.DeploymentResult;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Load;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer.PrivateServiceInfo;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ProxyResponse;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.QuotaUsage;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ServiceConvergence;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.FingerPrint;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TesterCloud;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.api.integration.noderepository.RestartFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockTesterCloud;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.net.URI;
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import java.util.logging.Level;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
-import static com.yahoo.config.provision.NodeResources.DiskSpeed.slow;
-import static com.yahoo.config.provision.NodeResources.StorageType.remote;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * @author mortent
- * @author jonmv
- */
-public class ConfigServerMock extends AbstractComponent implements ConfigServer {
-
- private final MockTesterCloud mockTesterCloud;
- private final Map<DeploymentId, Application> applications = new LinkedHashMap<>();
- private final Set<ZoneId> inactiveZones = new HashSet<>();
- private final Map<DeploymentId, EndpointStatus> endpoints = new HashMap<>();
- private final NodeRepositoryMock nodeRepository = new NodeRepositoryMock();
- private final Map<DeploymentId, ServiceConvergence> serviceStatus = new HashMap<>();
- private final Set<ApplicationId> disallowConvergenceCheckApplications = new HashSet<>();
- private final Version initialVersion = new Version(6, 1, 0);
- private final DockerImage initialDockerImage = DockerImage.fromString("registry.example.com/vespa/vespa:6.1.0");
- private final Set<DeploymentId> suspendedApplications = new HashSet<>();
- private final Map<ZoneId, Set<LoadBalancer>> loadBalancers = new HashMap<>();
- private final Set<Environment> deferLoadBalancerProvisioning = new HashSet<>();
- private final Map<DeploymentId, List<DeploymentResult.LogEntry>> warnings = new HashMap<>();
- private final Map<DeploymentId, Set<ContainerEndpoint>> containerEndpoints = new HashMap<>();
- private final Map<DeploymentId, List<ClusterMetrics>> clusterMetrics = new HashMap<>();
- private final Map<DeploymentId, TestReport> testReport = new HashMap<>();
- private final Map<DeploymentId, CloudAccount> cloudAccounts = new HashMap<>();
- private final Map<DeploymentId, List<X509Certificate>> additionalCertificates = new HashMap<>();
- private final Map<HostName, Map<TokenId, List<FingerPrint>>> activeTokenFingerprints = new HashMap<>();
- private List<SearchNodeMetrics> searchNodeMetrics;
-
- private Version lastPrepareVersion = null;
- private Consumer<ApplicationId> prepareException = null;
- private Supplier<String> log = () -> "INFO - All good";
-
- public ConfigServerMock(ZoneRegistryMock zoneRegistry, NameService nameService) {
- bootstrap(zoneRegistry.zones().all().ids(), SystemApplication.notController());
- this.mockTesterCloud = new MockTesterCloud(nameService);
- }
-
- /** Assigns a reserved tenant node to the given deployment, with initial versions. */
- public void provision(ZoneId zone, ApplicationId application, ClusterSpec.Id clusterId) {
- var current = new ClusterResources(2, 1, new NodeResources(2, 8, 50, 1, slow, remote));
- Cluster cluster = new Cluster(clusterId,
- ClusterSpec.Type.container,
- new ClusterResources(2, 1, new NodeResources(1, 4, 20, 1, slow, remote)),
- new ClusterResources(2, 1, new NodeResources(4, 16, 90, 1, slow, remote)),
- IntRange.to(3),
- current,
- new Cluster.Autoscaling("ideal",
- "Cluster is ideally scaled",
- Optional.of(new ClusterResources(2, 1, new NodeResources(3, 8, 50, 1, slow, remote))),
- Instant.ofEpochMilli(123),
- new Load(0.35, 0.65, 1.0),
- new Load(0.2, 0.5, 0.8),
- new Cluster.Autoscaling.Metrics(0.1, 0.2, 0.3)),
- Cluster.Autoscaling.empty(),
- List.of(new Cluster.ScalingEvent(new ClusterResources(0, 0, NodeResources.unspecified()),
- current,
- Instant.ofEpochMilli(1234),
- Optional.of(Instant.ofEpochMilli(2234)))),
- Duration.ofMinutes(6));
- nodeRepository.putApplication(zone,
- new com.yahoo.vespa.hosted.controller.api.integration.configserver.Application(application,
- List.of(cluster)));
-
- Node parent = nodeRepository().list(zone, NodeFilter.all().applications(SystemApplication.tenantHost.id())).stream().findAny()
- .orElseThrow(() -> new IllegalStateException("No parent hosts in " + zone));
- nodeRepository().putNodes(zone, Node.builder().hostname(hostFor(application, zone))
- .state(Node.State.reserved)
- .type(NodeType.tenant)
- .owner(application)
- .parentHostname(parent.hostname())
- .currentVersion(initialVersion)
- .wantedVersion(initialVersion)
- .currentDockerImage(initialDockerImage)
- .wantedDockerImage(initialDockerImage)
- .currentOsVersion(Version.emptyVersion)
- .wantedOsVersion(Version.emptyVersion)
- .resources(new NodeResources(2, 8, 50, 1, slow, remote))
- .serviceState(Node.ServiceState.unorchestrated)
- .flavor("d-2-8-50")
- .clusterId(clusterId.value())
- .clusterType(Node.ClusterType.container)
- .build());
- }
-
- public HostName hostFor(ApplicationId application, ZoneId zone) {
- return HostName.of("host-" + application.toFullString() + "-" + zone.value());
- }
-
- public void bootstrap(List<ZoneId> zones, SystemApplication... applications) {
- bootstrap(zones, List.of(applications));
- }
-
- public void bootstrap(List<ZoneId> zones, List<SystemApplication> applications) {
- nodeRepository().clear();
- addNodes(zones, applications);
- }
-
- public void addNodes(List<ZoneId> zones, List<SystemApplication> applications) {
- for (ZoneId zone : zones) {
- for (SystemApplication application : applications) {
- for (int i = 1; i <= 3; i++) {
- Node node = Node.builder()
- .hostname(HostName.of("node-" + i + "-" + application.id().application()
- .value() + "-" + zone.value()))
- .state(Node.State.active)
- .type(application.nodeType())
- .owner(application.id())
- .currentVersion(initialVersion).wantedVersion(initialVersion)
- .currentOsVersion(Version.emptyVersion).wantedOsVersion(Version.emptyVersion)
- .build();
- nodeRepository().putNodes(zone, node);
- }
- convergeServices(application.id(), zone);
- }
- }
- }
-
- /** Converge all services belonging to the given application */
- public void convergeServices(ApplicationId application, ZoneId zone) {
- List<Node> nodes = nodeRepository.list(zone, NodeFilter.all().applications(application));
- serviceStatus.put(new DeploymentId(application, zone), new ServiceConvergence(application,
- zone,
- true,
- 2,
- nodes.stream()
- .map(node -> new ServiceConvergence.Status(node.hostname(),
- 43,
- "container",
- 2))
- .toList()));
- }
-
- /** The version given in the previous prepare call, or empty if no call has been made */
- public Optional<Version> lastPrepareVersion() {
- return Optional.ofNullable(lastPrepareVersion);
- }
-
- /** Sets a function that may throw, determined by app id. */
- public void throwOnPrepare(Consumer<ApplicationId> prepareThrower) {
- this.prepareException = prepareThrower;
- }
-
- /** The exception to throw on the next prepare run, or null to continue normally */
- public void throwOnNextPrepare(RuntimeException prepareException) {
- this.prepareException = prepareException == null ? null : id -> { this.prepareException = null; throw prepareException; };
- }
-
- /** Set version for an application in a given zone */
- public void setVersion(Version version, ApplicationId application, ZoneId zone) {
- setVersion(zone, nodeRepository.list(zone, NodeFilter.all().applications(application)), version, false);
- }
-
- /** Set version for nodeCount number of nodes in application in a given zone */
- public void setVersion(Version version, List<Node> nodes, ZoneId zone) {
- setVersion(zone, nodes, version, false);
- }
-
- /** Set OS version for an application in a given zone */
- public void setOsVersion(Version version, ApplicationId application, ZoneId zone) {
- setVersion(zone, nodeRepository.list(zone, NodeFilter.all().applications(application)), version, true);
- }
-
- /** Set OS version for an application in a given zone */
- public void setOsVersion(Version version, List<Node> nodes, ZoneId zone) {
- setVersion(zone, nodes, version, true);
- }
-
- private void setVersion(ZoneId zone, List<Node> nodes, Version version, boolean osVersion) {
- for (var node : nodes) {
- Node newNode;
- if (osVersion) {
- newNode = Node.builder(node).currentOsVersion(version).wantedOsVersion(version).build();
- } else {
- newNode = Node.builder(node).currentVersion(version).wantedVersion(version).build();
- }
- nodeRepository().putNodes(zone, newNode);
- }
- }
-
- /** The initial version of this config server */
- public Version initialVersion() {
- return initialVersion;
- }
-
- /** Get deployed application by ID */
- public Optional<Application> application(ApplicationId id, ZoneId zone) {
- return Optional.ofNullable(applications.get(new DeploymentId(id, zone)));
- }
-
- @Override
- public void setSuspension(DeploymentId deployment, boolean suspend) {
- if (suspend)
- suspendedApplications.add(deployment);
- else
- suspendedApplications.remove(deployment);
- }
-
- public void generateWarnings(DeploymentId deployment, int count) {
- warnings.put(deployment,
- IntStream.rangeClosed(1, count)
- .mapToObj(i -> new DeploymentResult.LogEntry(Instant.now().toEpochMilli(),
- "log message " + i + " generated by unit test",
- Level.WARNING,
- false))
- .toList());
- }
-
- public Map<DeploymentId, Set<ContainerEndpoint>> containerEndpoints() {
- return Collections.unmodifiableMap(containerEndpoints);
- }
-
- public Optional<CloudAccount> cloudAccount(DeploymentId deployment) {
- return Optional.ofNullable(cloudAccounts.get(deployment));
- }
-
- public Set<String> containerEndpointNames(DeploymentId deployment) {
- return containerEndpoints.getOrDefault(deployment, Set.of()).stream()
- .map(ContainerEndpoint::names)
- .flatMap(Collection::stream)
- .collect(Collectors.toUnmodifiableSet());
- }
-
- public void setMetrics(DeploymentId deployment, ClusterMetrics clusterMetrics) {
- setMetrics(deployment, List.of(clusterMetrics));
- }
-
- public void setMetrics(DeploymentId deployment, List<ClusterMetrics> clusterMetrics) {
- this.clusterMetrics.put(deployment, clusterMetrics);
- }
-
- public void setProtonMetrics(List<SearchNodeMetrics> searchnodeMetrics) {
- this.searchNodeMetrics = searchnodeMetrics;
- }
-
- public void deferLoadBalancerProvisioningIn(Set<Environment> environments) {
- deferLoadBalancerProvisioning.addAll(environments);
- }
-
- public List<X509Certificate> additionalCertificates(DeploymentId deployment) {
- return additionalCertificates.getOrDefault(deployment, List.of());
- }
-
- public void setActiveTokenFingerprints(HostName hostname, Map<TokenId, List<FingerPrint>> tokens) {
- activeTokenFingerprints.put(hostname, tokens);
- }
-
- @Override
- public NodeRepositoryMock nodeRepository() {
- return nodeRepository;
- }
-
- @Override
- public Optional<ServiceConvergence> serviceConvergence(DeploymentId deployment, Optional<Version> version) {
- if (disallowConvergenceCheckApplications.contains(deployment.applicationId()))
- throw new IllegalStateException(deployment.applicationId() + " should not ask for service convergence");
-
- return Optional.ofNullable(serviceStatus.get(deployment));
- }
-
- public void disallowConvergenceCheck(ApplicationId applicationId) {
- disallowConvergenceCheckApplications.add(applicationId);
- }
-
- private Set<LoadBalancer> getLoadBalancers(ZoneId zone) {
- return loadBalancers.getOrDefault(zone, new LinkedHashSet<>());
- }
-
- @Override
- public List<LoadBalancer> getLoadBalancers(ApplicationId application, ZoneId zone) {
- return getLoadBalancers(zone).stream()
- .filter(lb -> lb.application().equals(application))
- .toList();
- }
-
- @Override
- public List<FlagData> listFlagData(ZoneId zone) {
- return List.of();
- }
-
- @Override
- public TesterCloud.Status getTesterStatus(DeploymentId deployment) {
- return TesterCloud.Status.SUCCESS;
- }
-
- @Override
- public String startTests(DeploymentId deployment, TesterCloud.Suite suite, byte[] config) {
- return "Tests started";
- }
-
- @Override
- public List<LogEntry> getTesterLog(DeploymentId deployment, long after) {
- return List.of();
- }
-
- @Override
- public boolean isTesterReady(DeploymentId deployment) {
- return false;
- }
-
- @Override
- public Optional<TestReport> getTestReport(DeploymentId deployment) {
- return Optional.ofNullable(testReport.get(deployment));
- }
-
- @Override
- public Availability verifyEndpoints(DeploymentId deploymentId, List<Endpoint> zoneEndpoints) {
- return mockTesterCloud.verifyEndpoints(deploymentId, zoneEndpoints, false); // Wraps the same name service mock, which is updated by test harness.
- }
-
- /** Add any of given loadBalancers that do not already exist to the load balancers in zone */
- public void putLoadBalancers(ZoneId zone, List<LoadBalancer> loadBalancers) {
- this.loadBalancers.computeIfAbsent(zone, __ -> new LinkedHashSet<>()).addAll(loadBalancers);
- }
-
- public void removeLoadBalancers(ApplicationId application, ZoneId zone) {
- getLoadBalancers(zone).removeIf(lb -> lb.application().equals(application));
- }
-
- @Override
- public PreparedApplication deploy(DeploymentData deployment) {
- byte[] appPackage;
- try (InputStream in = deployment.applicationPackage()) {
- appPackage = in.readAllBytes();
- }
- catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- deployment.cloudAccount(); // Supplier with side effects >_<
- lastPrepareVersion = deployment.platform();
- if (prepareException != null)
- prepareException.accept(ApplicationId.from(deployment.instance().tenant(),
- deployment.instance().application(),
- deployment.instance().instance()));
- DeploymentId id = new DeploymentId(deployment.instance(), deployment.zone());
-
- applications.put(id, new Application(id.applicationId(), lastPrepareVersion, appPackage));
- ClusterSpec.Id cluster = ClusterSpec.Id.from("default");
-
- if (nodeRepository().list(id.zoneId(), NodeFilter.all().applications(id.applicationId())).isEmpty())
- provision(id.zoneId(), id.applicationId(), cluster);
-
- this.containerEndpoints.put(id, deployment.endpoints().endpoints());
- deployment.cloudAccount().ifPresent(account -> this.cloudAccounts.put(id, account));
-
- if (!deferLoadBalancerProvisioning.contains(id.zoneId().environment())) {
- putLoadBalancers(id.zoneId(), List.of(new LoadBalancer(id.dottedString() + "." + cluster,
- id.applicationId(),
- cluster,
- Optional.of(HostName.of("lb-0--" + id.applicationId().toFullString() + "--" + id.zoneId().toString())),
- Optional.empty(),
- LoadBalancer.State.active,
- Optional.of("dns-zone-1"),
- Optional.empty(),
- Optional.of(new PrivateServiceInfo("service", List.of(new AllowedUrn(AccessType.awsPrivateLink, "arne")))),
- true)));
- }
-
- Application application = applications.get(id);
- application.activate();
- List<Node> nodes = nodeRepository.list(id.zoneId(), NodeFilter.all().applications(id.applicationId()));
- for (Node node : nodes) {
- nodeRepository.putNodes(id.zoneId(), Node.builder(node)
- .state(Node.State.active)
- .wantedVersion(application.version().get())
- .build());
- }
- serviceStatus.put(id, new ServiceConvergence(id.applicationId(),
- id.zoneId(),
- false,
- 2,
- nodes.stream()
- .map(node -> new ServiceConvergence.Status(node.hostname(),
- 43,
- "container",
- 1))
- .toList()));
-
- additionalCertificates.put(id, deployment.operatorCertificates());
- DeploymentResult result = new DeploymentResult("foo", warnings.getOrDefault(id, List.of()));
- return () -> result;
- }
-
- @Override
- public void reindex(DeploymentId deployment, List<String> clusterNames, List<String> documentTypes, boolean indexedOnly, Double speed, String cause) { }
-
- @Override
- public ApplicationReindexing getReindexing(DeploymentId deployment) {
- return new ApplicationReindexing(true,
- Map.of("cluster",
- new ApplicationReindexing.Cluster(Map.of("type", 100L),
- Map.of("type", new Status(Instant.ofEpochMilli(345),
- Instant.ofEpochMilli(456),
- Instant.ofEpochMilli(567),
- ApplicationReindexing.State.FAILED,
- "(#`д´)ノ",
- 0.1,
- 1.0,
- "test reindexing")))));
- }
-
- @Override
- public void disableReindexing(DeploymentId deployment) { }
-
- @Override
- public void enableReindexing(DeploymentId deployment) { }
-
- @Override
- public boolean isSuspended(DeploymentId deployment) {
- return suspendedApplications.contains(deployment);
- }
-
- @Override
- public void restart(DeploymentId deployment, RestartFilter restartFilter) {
- nodeRepository().requestRestart(deployment, restartFilter.getHostName());
- }
-
- @Override
- public void deactivate(DeploymentId deployment) {
- ApplicationId applicationId = deployment.applicationId();
- nodeRepository().removeNodes(deployment.zoneId(),
- nodeRepository().list(deployment.zoneId(), NodeFilter.all().applications(applicationId)));
- if ( ! applications.containsKey(deployment))
- return;
-
- applications.remove(deployment);
- serviceStatus.remove(deployment);
-
- // This simulates what a real config server does: It deactivates the LB. Actual removal happens in the background
- loadBalancers.computeIfPresent(deployment.zoneId(), (k, old) ->
- old.stream().map(lb -> lb.application().equals(deployment.applicationId())
- ? new LoadBalancer(lb.id(), lb.application(), lb.cluster(), lb.hostname(), lb.ipAddress(),
- LoadBalancer.State.inactive, lb.dnsZone(), lb.cloudAccount(),
- lb.service(), lb.isPublic())
- : lb)
- .collect(Collectors.toCollection(LinkedHashSet::new)));
- }
-
- @Override
- public List<ClusterMetrics> getDeploymentMetrics(DeploymentId deployment) {
- return Collections.unmodifiableList(clusterMetrics.getOrDefault(deployment, List.of()));
- }
-
- @Override
- public List<SearchNodeMetrics> getSearchNodeMetrics(DeploymentId deployment) {
- return this.searchNodeMetrics;
- }
-
- @Override
- public ProxyResponse getServiceNodePage(DeploymentId deployment, String serviceName, DomainName node, Path subPath, Query query) {
- return new ProxyResponse((subPath + " and " + query).getBytes(UTF_8), "text/html", 200);
- }
-
- @Override
- public ProxyResponse getServiceNodes(DeploymentId deployment) {
- return new ProxyResponse("{\"json\":\"thank you very much\"}".getBytes(UTF_8), "application.json", 200);
- }
-
- @Override
- public void setGlobalRotationStatus(DeploymentId deployment, List<String> upstreamNames, EndpointStatus status) {
- endpoints.put(deployment, status);
- }
-
- @Override
- public void setGlobalRotationStatus(ZoneId zone, boolean in) {
- if (in) {
- inactiveZones.remove(zone);
- } else {
- inactiveZones.add(zone);
- }
- }
-
- @Override
- public EndpointStatus getGlobalRotationStatus(DeploymentId deployment, String upstreamName) {
- EndpointStatus status = new EndpointStatus(EndpointStatus.Status.in, "", Instant.ofEpochSecond(1497618757L));
- return endpoints.getOrDefault(deployment, status);
- }
-
- @Override
- public boolean getGlobalRotationStatus(ZoneId zone) {
- return !inactiveZones.contains(zone);
- }
-
- @Override
- public InputStream getLogs(DeploymentId deployment, Map<String, String> queryParameters) {
- return new ByteArrayInputStream(log.get().getBytes(UTF_8));
- }
-
- @Override
- public ProxyResponse getApplicationPackageContent(DeploymentId deployment, Path path, URI requestUri) {
- return new ProxyResponse(("{\"path\":\"/" + String.join("/", path.segments()) + "\"}").getBytes(UTF_8), "application/json", 200);
- }
-
- public void setLogStream(Supplier<String> log) {
- this.log = log;
- }
-
- @Override
- public List<String> getContentClusters(DeploymentId deployment) {
- return Collections.singletonList("music");
- }
-
- @Override
- public QuotaUsage getQuotaUsage(DeploymentId deploymentId) {
- return new QuotaUsage(42);
- }
-
- @Override
- public String validateSecretStore(DeploymentId deployment, TenantSecretStore tenantSecretStore, String region, String parameterName) {
- return "{\"settings\":{\"name\":\"foo\",\"role\":\"vespa-secretstore-access\",\"awsId\":\"892075328880\",\"externalId\":\"*****\",\"region\":\"us-east-1\"},\"status\":\"ok\"}";
- }
-
- @Override
- public Map<HostName, Map<TokenId, List<FingerPrint>>> activeTokenFingerprints(DeploymentId deploymentId) {
- return activeTokenFingerprints;
- }
-
- public static class Application {
-
- private final ApplicationId id;
- private final Version version;
- private boolean activated;
- private final byte[] applicationPackage;
-
- private Application(ApplicationId id, Version version, byte[] applicationPackage) {
- this.id = id;
- this.version = version;
- this.applicationPackage = applicationPackage;
- }
-
- public ApplicationId id() {
- return id;
- }
-
- public Optional<Version> version() {
- return Optional.ofNullable(version);
- }
-
- public boolean activated() {
- return activated;
- }
-
- public ApplicationPackage applicationPackage() {
- return new ApplicationPackage(applicationPackage);
- }
-
- private void activate() {
- this.activated = true;
- }
-
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerProxyMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerProxyMock.java
deleted file mode 100644
index 29aeb3f755d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ConfigServerProxyMock.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.restapi.StringResponse;
-import com.yahoo.vespa.hosted.controller.proxy.ConfigServerRestExecutor;
-import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
-
-import java.io.InputStream;
-import java.util.Optional;
-import java.util.Scanner;
-
-/**
- * @author mpolden
- */
-public class ConfigServerProxyMock extends AbstractComponent implements ConfigServerRestExecutor {
-
- private volatile ProxyRequest lastReceived = null;
- private volatile String requestBody = null;
-
- @Override
- public HttpResponse handle(ProxyRequest request) {
- lastReceived = request;
- // Copy request body as the input stream is drained once the request completes
- requestBody = asString(request.getData());
- return new StringResponse("ok");
- }
-
- public Optional<ProxyRequest> lastReceived() {
- return Optional.ofNullable(lastReceived);
- }
-
- public Optional<String> lastRequestBody() {
- return Optional.ofNullable(requestBody);
- }
-
- private static String asString(InputStream inputStream) {
- Scanner scanner = new Scanner(inputStream).useDelimiter("\\A");
- return scanner.hasNext() ? scanner.next() : "";
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/MetricsMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/MetricsMock.java
deleted file mode 100644
index 8172de8680d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/MetricsMock.java
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.jdisc.Metric;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-/**
- * @author mortent
- */
-public class MetricsMock implements Metric {
-
- private final LinkedHashMap<Context, Map<String, Number>> metrics = new LinkedHashMap<>();
-
- @Override
- public void set(String key, Number val, Context ctx) {
- metrics.putIfAbsent(ctx, new HashMap<>());
- Map<String, Number> metricsMap = metrics.get(ctx);
- metricsMap.put(key, val);
- }
-
- @Override
- public void add(String key, Number val, Context ctx) {
- Map<String, Number> metricsMap = metrics.getOrDefault(ctx, new HashMap<>());
- metricsMap.compute(key, (k, v) -> v == null ? val : sum(v, val));
- }
-
- private Number sum(Number n1, Number n2) {
- return n1.doubleValue() + n2.doubleValue();
- }
-
- @Override
- @SuppressWarnings("unchecked")
- public Context createContext(Map<String, ?> properties) {
- Context ctx = new MapContext((Map<String, String>) properties);
- metrics.putIfAbsent(ctx, new HashMap<>());
- return ctx;
- }
-
- /** Returns a zero-context metric by name, or null if it is not present */
- public Number getMetric(String name) {
- Map<String, Number> valuesForEmptyContext = metrics.get(createContext(Collections.emptyMap()));
- if (valuesForEmptyContext == null) return null;
- return valuesForEmptyContext.get(name);
- }
-
- /** Returns metric and context for any metric matching the given dimension predicate */
- public Map<MapContext, Map<String, Number>> getMetrics(Predicate<Map<String, String>> dimensionMatcher) {
- return metrics.entrySet()
- .stream()
- .filter(context -> dimensionMatcher.test(((MapContext) context.getKey()).getDimensions()))
- .collect(Collectors.toMap(kv -> (MapContext) kv.getKey(),
- Map.Entry::getValue,
- (v1, v2) -> { throw new IllegalStateException("Duplicate keys for values '" + v1 + "' and '" + v2 + "'."); },
- LinkedHashMap::new));
- }
-
- /** Returns the most recently added metric matching given dimension and name */
- public Optional<Number> getMetric(Predicate<Map<String, String>> dimensionMatcher, String name) {
- var metrics = List.copyOf(getMetrics(dimensionMatcher).values());
- for (int i = metrics.size() - 1; i >= 0; i--) {
- var metric = metrics.get(i).get(name);
- if (metric != null) return Optional.of(metric);
- }
- return Optional.empty();
- }
-
- /** Returns the most recently added metric for given instance */
- public Optional<Number> getMetric(ApplicationId instance, String name) {
- return getMetric(d -> instance.toFullString().equals(d.get("applicationId")), name);
- }
-
- public static class MapContext implements Context {
-
- private final Map<String, String> dimensions;
-
- public MapContext(Map<String, String> dimensions) {
- this.dimensions = dimensions;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- MapContext that = (MapContext) o;
- return dimensions.equals(that.dimensions);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(dimensions);
- }
-
- public Map<String, String> getDimensions() {
- return dimensions;
- }
-
- }
-}
-
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java
deleted file mode 100644
index a4b14755d7d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/NodeRepositoryMock.java
+++ /dev/null
@@ -1,378 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.yahoo.collections.Pair;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveUriUpdate;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Application;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationStats;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ArchiveUris;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Load;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepoStats;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.TargetVersions;
-import com.yahoo.vespa.hosted.controller.api.integration.noderepository.ApplicationPatch;
-
-import java.net.URI;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.TreeMap;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.UnaryOperator;
-import java.util.stream.Collectors;
-
-/**
- * @author mpolden
- * @author jonmv
- */
-public class NodeRepositoryMock implements NodeRepository {
-
- private final Map<ZoneId, Map<HostName, Node>> nodeRepository = new ConcurrentHashMap<>();
- private final Map<ZoneId, Map<ApplicationId, Application>> applications = new ConcurrentHashMap<>();
- private final Map<ZoneId, TargetVersions> targetVersions = new ConcurrentHashMap<>();
- private final Map<DeploymentId, Pair<Double, Double>> trafficFractions = new ConcurrentHashMap<>();
- private final Map<DeploymentClusterId, BcpGroupInfo> bcpGroupInfos = new ConcurrentHashMap<>();
- private final Map<ZoneId, ArchiveUris> archiveUris = new ConcurrentHashMap<>();
-
- private boolean allowPatching = true;
- private boolean hasSpareCapacity = false;
-
- @Override
- public void addNodes(ZoneId zone, List<Node> nodes) {
- Map<HostName, Node> existingNodes = nodeRepository.getOrDefault(zone, Map.of());
- for (var node : nodes) {
- if (existingNodes.containsKey(node.hostname())) {
- throw new IllegalArgumentException("Node " + node.hostname() + " already added in zone " + zone);
- }
- }
- putNodes(zone, nodes);
- }
-
- @Override
- public void deleteNode(ZoneId zone, String hostname) {
- require(zone, hostname);
- nodeRepository.get(zone).remove(HostName.of(hostname));
- }
-
- @Override
- public void setState(ZoneId zone, Node.State state, String hostname) {
- Node node = Node.builder(require(zone, hostname))
- .state(Node.State.valueOf(state.name()))
- .build();
- putNodes(zone, node);
- }
-
- @Override
- public Node getNode(ZoneId zone, String hostname) {
- return require(zone, hostname);
- }
-
- @Override
- public List<Node> list(ZoneId zone, NodeFilter filter) {
- return nodeRepository.getOrDefault(zone, Map.of()).values().stream()
- .filter(node -> filter.includeDeprovisioned() || node.state() != Node.State.deprovisioned)
- .filter(node -> filter.applications().isEmpty() ||
- (node.owner().isPresent() && filter.applications().contains(node.owner().get())))
- .filter(node -> filter.hostnames().isEmpty() || filter.hostnames().contains(node.hostname()))
- .filter(node -> filter.states().isEmpty() || filter.states().contains(node.state()))
- .filter(node -> filter.clusterIds().isEmpty() || filter.clusterIds().contains(ClusterSpec.Id.from(node.clusterId())))
- .filter(node -> filter.clusterTypes().isEmpty() || filter.clusterTypes().contains(node.clusterType()))
- .toList();
- }
-
- @Override
- public Application getApplication(ZoneId zone, ApplicationId applicationId) {
- return applications.get(zone).get(applicationId);
- }
-
- @Override
- public void patchApplication(ZoneId zone, ApplicationId application, ApplicationPatch applicationPatch) {
- trafficFractions.put(new DeploymentId(application, zone),
- new Pair<>(applicationPatch.currentReadShare, applicationPatch.maxReadShare));
- if (applicationPatch.clusters != null) {
- for (var cluster : applicationPatch.clusters.entrySet())
- bcpGroupInfos.put(new DeploymentClusterId(new DeploymentId(application, zone), new ClusterSpec.Id(cluster.getKey())),
- new BcpGroupInfo(cluster.getValue().bcpGroupInfo.queryRate,
- cluster.getValue().bcpGroupInfo.growthRateHeadroom,
- cluster.getValue().bcpGroupInfo.cpuCostPerQuery));
- }
- }
-
- @Override
- public NodeRepoStats getStats(ZoneId zone) {
- List<ApplicationStats> applicationStats =
- applications.containsKey(zone)
- ? applications.get(zone).keySet().stream()
- .map(id -> new ApplicationStats(id, Load.zero(), 0, 0))
- .toList()
- : List.of();
-
- return new NodeRepoStats(0.0, 0.0, Load.zero(), Load.zero(), applicationStats);
- }
-
- @Override
- public ArchiveUris getArchiveUris(ZoneId zone) {
- return archiveUris.getOrDefault(zone, ArchiveUris.EMPTY);
- }
-
- @Override
- public void updateArchiveUri(ZoneId zone, ArchiveUriUpdate update) {
- archiveUris.compute(zone, (z, prev) -> {
- prev = prev == null ? ArchiveUris.EMPTY : prev;
- if (update.tenantName().isPresent()) {
- Map<TenantName, URI> updated = new HashMap<>(prev.tenantArchiveUris());
- update.archiveUri().ifPresentOrElse(uri -> updated.put(update.tenantName().get(), uri),
- () -> updated.remove(update.tenantName().get()));
- return new ArchiveUris(updated, prev.accountArchiveUris());
- } else {
- Map<CloudAccount, URI> updated = new HashMap<>(prev.accountArchiveUris());
- update.archiveUri().ifPresentOrElse(uri -> updated.put(update.cloudAccount().get(), uri),
- () -> updated.remove(update.cloudAccount().get()));
- return new ArchiveUris(prev.tenantArchiveUris(), updated);
- }
- });
- }
-
- @Override
- public void upgrade(ZoneId zone, NodeType type, Version version, boolean allowDowngrade) {
- this.targetVersions.compute(zone, (ignored, targetVersions) -> {
- if (targetVersions == null) {
- targetVersions = TargetVersions.EMPTY;
- }
- Optional<Version> current = targetVersions.vespaVersion(type);
- if (current.isPresent() && version.isBefore(current.get()) && !allowDowngrade) {
- throw new IllegalArgumentException("Changing wanted version for " + type + " in " + zone + " from " +
- current.get() + " to " + version +
- ", but downgrade is not allowed");
- }
- return targetVersions.withVespaVersion(type, version);
- });
- // Bump wanted version of each node. This is done by InfrastructureProvisioner in a real node repository.
- nodeRepository.getOrDefault(zone, Map.of()).values()
- .stream()
- .filter(node -> node.type() == type)
- .map(node -> Node.builder(node).wantedVersion(version).build())
- .forEach(node -> putNodes(zone, node));
- }
-
- @Override
- public void upgradeOs(ZoneId zone, NodeType type, Version version, boolean downgrade) {
- this.targetVersions.compute(zone, (ignored, targetVersions) -> {
- if (targetVersions == null) {
- targetVersions = TargetVersions.EMPTY;
- }
- return targetVersions.withOsVersion(type, version);
- });
- // Bump wanted version of each node. This is done by OsUpgradeActivator in a real node repository.
- nodeRepository.getOrDefault(zone, Map.of()).values()
- .stream()
- .filter(node -> node.type() == type)
- .map(node -> Node.builder(node).wantedOsVersion(version).build())
- .forEach(node -> putNodes(zone, node));
- }
-
- @Override
- public TargetVersions targetVersionsOf(ZoneId zone) {
- return targetVersions.getOrDefault(zone, TargetVersions.EMPTY);
- }
-
- @Override
- public void requestFirmwareCheck(ZoneId zone) {
- }
-
- @Override
- public void cancelFirmwareCheck(ZoneId zone) {
- }
-
- @Override
- public void retire(ZoneId zone, String hostname, boolean wantToRetire, boolean wantToDeprovision) {
- patchNodes(zone, hostname, (node) -> Node.builder(node).wantToRetire(wantToRetire).wantToDeprovision(wantToDeprovision).build());
- }
-
- @Override
- public void dropDocuments(ZoneId zoneId, ApplicationId applicationId, Optional<ClusterSpec.Id> clusterId) {
- }
-
- @Override
- public void updateReports(ZoneId zone, String hostname, Map<String, String> reports) {
- Map<String, String> trimmedReports = reports.entrySet().stream()
- // Null value clears a report
- .filter(kv -> kv.getValue() != null)
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
- patchNodes(zone, hostname, (node) -> Node.builder(node).reports(trimmedReports).build());
- }
-
- @Override
- public void updateModel(ZoneId zone, String hostname, String modelName) {
- patchNodes(zone, hostname, (node) -> Node.builder(node).modelName(modelName).build());
- }
-
- @Override
- public void updateSwitchHostname(ZoneId zone, String hostname, String switchHostname) {
- patchNodes(zone, hostname, (node) -> Node.builder(node).switchHostname(switchHostname).build());
- }
-
- @Override
- public void reboot(ZoneId zone, String hostname) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public boolean isReplaceable(ZoneId zone, List<HostName> hostnames) {
- return hasSpareCapacity;
- }
-
- /** Add or update given nodes in zone */
- public void putNodes(ZoneId zone, List<Node> nodes) {
- Map<HostName, Node> zoneNodes = nodeRepository.computeIfAbsent(zone, (k) -> new ConcurrentHashMap<>());
- for (var node : nodes) {
- zoneNodes.put(node.hostname(), node);
- }
- }
-
- /** Add or update given node in zone */
- public void putNodes(ZoneId zone, Node node) {
- putNodes(zone, List.of(node));
- }
-
- public void putApplication(ZoneId zone, Application application) {
- applications.computeIfAbsent(zone, (k) -> new TreeMap<>())
- .put(application.id(), application);
- }
-
- public Pair<Double, Double> getTrafficFraction(ApplicationId application, ZoneId zone) {
- return trafficFractions.get(new DeploymentId(application, zone));
- }
-
- public BcpGroupInfo getBcpGroupInfo(ApplicationId application, ZoneId zone, ClusterSpec.Id cluster) {
- return bcpGroupInfos.get(new DeploymentClusterId(new DeploymentId(application, zone), cluster));
- }
-
- /** Remove given nodes from zone */
- public void removeNodes(ZoneId zone, List<Node> nodes) {
- nodes.forEach(node -> nodeRepository.get(zone).remove(node.hostname()));
- }
-
- /** Remove all nodes in all zones */
- public void clear() {
- nodeRepository.clear();
- }
-
- /** Add a fixed set of nodes to given zone */
- public void addFixedNodes(ZoneId zone) {
- var nodeA = Node.builder()
- .hostname(HostName.of("hostA"))
- .parentHostname(HostName.of("parentHostA"))
- .state(Node.State.active)
- .type(NodeType.tenant)
- .owner(ApplicationId.from("tenant1", "app1", "default"))
- .currentVersion(Version.fromString("7.42"))
- .wantedVersion(Version.fromString("7.42"))
- .currentOsVersion(Version.fromString("7.6"))
- .wantedOsVersion(Version.fromString("7.6"))
- .serviceState(Node.ServiceState.expectedUp)
- .resources(new NodeResources(24, 24, 500, 1))
- .clusterId("clusterA")
- .clusterType(Node.ClusterType.container)
- .exclusiveTo(ApplicationId.from("t1", "a1", "i1"))
- .build();
- var nodeB = Node.builder()
- .hostname(HostName.of("hostB"))
- .parentHostname(HostName.of("parentHostB"))
- .state(Node.State.active)
- .type(NodeType.tenant)
- .owner(ApplicationId.from("tenant2", "app2", "default"))
- .currentVersion(Version.fromString("7.42"))
- .wantedVersion(Version.fromString("7.42"))
- .currentOsVersion(Version.fromString("7.6"))
- .wantedOsVersion(Version.fromString("7.6"))
- .serviceState(Node.ServiceState.expectedUp)
- .resources(new NodeResources(40, 24, 500, 1))
- .cost(20)
- .clusterId("clusterB")
- .clusterType(Node.ClusterType.container)
- .build();
- putNodes(zone, List.of(nodeA, nodeB));
- }
-
- public void doUpgrade(DeploymentId deployment, Optional<HostName> hostName, Version version) {
- patchNodes(deployment, hostName, node -> {
- return Node.builder(node)
- .currentVersion(version)
- .currentDockerImage(node.wantedDockerImage())
- .build();
- });
- }
-
- public void requestRestart(DeploymentId deployment, Optional<HostName> hostname) {
- patchNodes(deployment, hostname, node -> Node.builder(node).wantedRestartGeneration(node.wantedRestartGeneration() + 1).build());
- }
-
- public void doRestart(DeploymentId deployment, Optional<HostName> hostname) {
- patchNodes(deployment, hostname, node -> Node.builder(node).restartGeneration(node.restartGeneration() + 1).build());
- }
-
- public void requestReboot(DeploymentId deployment, Optional<HostName> hostname) {
- patchNodes(deployment, hostname, node -> Node.builder(node).wantedRebootGeneration(node.wantedRebootGeneration() + 1).build());
- }
-
- public void doReboot(DeploymentId deployment, Optional<HostName> hostname) {
- patchNodes(deployment, hostname, node -> Node.builder(node).rebootGeneration(node.rebootGeneration() + 1).build());
- }
-
- public NodeRepositoryMock allowPatching(boolean allowPatching) {
- this.allowPatching = allowPatching;
- return this;
- }
-
- public void hasSpareCapacity(boolean hasSpareCapacity) {
- this.hasSpareCapacity = hasSpareCapacity;
- }
-
- private Node require(ZoneId zone, String hostname) {
- return require(zone, HostName.of(hostname));
- }
-
- private Node require(ZoneId zone, HostName hostname) {
- Node node = nodeRepository.getOrDefault(zone, Map.of()).get(hostname);
- if (node == null) throw new IllegalArgumentException("Node not found in " + zone + ": " + hostname);
- return node;
- }
-
- private void patchNodes(ZoneId zone, String hostname, UnaryOperator<Node> patcher) {
- patchNodes(zone, Optional.of(HostName.of(hostname)), patcher);
- }
-
- private void patchNodes(DeploymentId deployment, Optional<HostName> hostname, UnaryOperator<Node> patcher) {
- patchNodes(deployment.zoneId(), hostname, patcher);
- }
-
- private void patchNodes(ZoneId zone, Optional<HostName> hostname, UnaryOperator<Node> patcher) {
- if (!allowPatching) throw new UnsupportedOperationException("Patching is disabled in this mock");
- List<Node> nodes;
- if (hostname.isPresent()) {
- nodes = List.of(require(zone, hostname.get()));
- } else {
- nodes = list(zone, NodeFilter.all());
- }
- putNodes(zone, nodes.stream().map(patcher).toList());
- }
-
- public record DeploymentClusterId(DeploymentId deploymentId, ClusterSpec.Id clusterId) {}
-
- public record BcpGroupInfo(double queryRate, double growthRateHeadroom, double cpuCostPerQuery) {}
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/SecretStoreMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/SecretStoreMock.java
deleted file mode 100644
index 10f8d27772e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/SecretStoreMock.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.container.jdisc.secretstore.SecretStore;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-/**
- * @author mpolden
- */
-public class SecretStoreMock extends AbstractComponent implements SecretStore {
-
- private final Map<String, TreeMap<Integer, String>> secrets = new HashMap<>();
-
- public SecretStoreMock setSecret(String name, String value, int version) {
- TreeMap<Integer, String> values = secrets.getOrDefault(name, new TreeMap<>());
- values.put(version, value);
- secrets.put(name, values);
- return this;
- }
-
- public SecretStoreMock setSecret(String name, String value) {
- return setSecret(name, value, 1);
- }
-
- public SecretStoreMock clear() {
- secrets.clear();
- return this;
- }
-
- @Override
- public String getSecret(String key) {
- TreeMap<Integer, String> values = secrets.get(key);
- if (values == null || values.isEmpty()) {
- return null;
- }
- return values.lastEntry().getValue();
- }
-
- @Override
- public String getSecret(String key, int version) {
- return secrets.getOrDefault(key, new TreeMap<>()).get(version);
- }
-
- @Override
- public List<Integer> listSecretVersions(String key) {
- return List.copyOf(secrets.getOrDefault(key, new TreeMap<>()).keySet());
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
deleted file mode 100644
index 2d1f06f3602..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java
+++ /dev/null
@@ -1,332 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.yahoo.cloud.config.ConfigserverConfig;
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.component.Version;
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.test.ManualClock;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
-import com.yahoo.vespa.hosted.controller.api.integration.ServiceRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.MockArchiveService;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AccessControlService;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.MockAccessControlService;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.MockEnclaveAccessService;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.MockResourceTagger;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.MockRoleService;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClient;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClientMock;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingReporter;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingReporterMock;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.MockBillingController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistry;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistryMock;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProviderMock;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidator;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateValidatorMock;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.MemoryNameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.MockVpcEndpointService;
-import com.yahoo.vespa.hosted.controller.api.integration.entity.MemoryEntityService;
-import com.yahoo.vespa.hosted.controller.api.integration.horizon.HorizonClient;
-import com.yahoo.vespa.hosted.controller.api.integration.horizon.MockHorizonClient;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.MockContactRetriever;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.MockIssueHandler;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumerMock;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClient;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClientMock;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.EndpointSecretManager;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.GcpSecretStore;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.NoopEndpointSecretManager;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.NoopGcpSecretStore;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.NoopTenantSecretService;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.DummyOwnershipIssues;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.DummySystemMonitor;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockRunDataStore;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockTesterCloud;
-import com.yahoo.vespa.hosted.controller.api.integration.user.RoleMaintainer;
-import com.yahoo.vespa.hosted.controller.api.integration.user.RoleMaintainerMock;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.MockChangeRequestClient;
-
-import java.net.URI;
-import java.time.Instant;
-import java.util.Optional;
-
-/**
- * A mock implementation of a {@link ServiceRegistry} for testing purposes.
- *
- * @author mpolden
- */
-public class ServiceRegistryMock extends AbstractComponent implements ServiceRegistry {
-
- private final ManualClock clock = new ManualClock();
- private final ControllerVersion controllerVersion;
- private final ZoneRegistryMock zoneRegistryMock;
- private final ConfigServerMock configServerMock;
- private final ConsoleUrls consoleUrls = new ConsoleUrls(URI.create("https://console.tld"));
- private final MemoryNameService memoryNameService = new MemoryNameService();
- private final MockVpcEndpointService vpcEndpointService = new MockVpcEndpointService(clock, memoryNameService);
- private final MockMailer mockMailer = new MockMailer();
- private final EndpointCertificateProviderMock endpointCertificateProviderMock = new EndpointCertificateProviderMock();
- private final EndpointCertificateValidatorMock endpointCertificateValidatorMock = new EndpointCertificateValidatorMock();
- private final MockContactRetriever mockContactRetriever = new MockContactRetriever();
- private final MockIssueHandler mockIssueHandler = new MockIssueHandler();
- private final DummyOwnershipIssues dummyOwnershipIssues = new DummyOwnershipIssues();
- private final LoggingDeploymentIssues loggingDeploymentIssues = new LoggingDeploymentIssues();
- private final MemoryEntityService memoryEntityService = new MemoryEntityService();
- private final DummySystemMonitor systemMonitor = new DummySystemMonitor();
- private final CostReportConsumerMock costReportConsumerMock = new CostReportConsumerMock();
- private final ArtifactRepositoryMock artifactRepositoryMock = new ArtifactRepositoryMock();
- private final MockTesterCloud mockTesterCloud;
- private final ApplicationStoreMock applicationStoreMock = new ApplicationStoreMock();
- private final MockRunDataStore mockRunDataStore = new MockRunDataStore();
- private final MockEnclaveAccessService mockAMIService = new MockEnclaveAccessService();
- private final MockResourceTagger mockResourceTagger = new MockResourceTagger();
- private final MockRoleService roleService = new MockRoleService();
- private final ArtifactRegistryMock containerRegistry = new ArtifactRegistryMock();
- private final NoopTenantSecretService tenantSecretService = new NoopTenantSecretService();
- private final NoopEndpointSecretManager secretManager = new NoopEndpointSecretManager();
- private final ArchiveService archiveService = new MockArchiveService(clock);
- private final MockChangeRequestClient changeRequestClient = new MockChangeRequestClient();
- private final AccessControlService accessControlService = new MockAccessControlService();
- private final HorizonClient horizonClient = new MockHorizonClient();
- private final PlanRegistry planRegistry = new PlanRegistryMock();
- private final ResourceDatabaseClient resourceDb = new ResourceDatabaseClientMock(planRegistry);
- private final BillingDatabaseClient billingDb = new BillingDatabaseClientMock(clock, planRegistry);
- private final BillingReporterMock billingReporter = new BillingReporterMock(clock, billingDb);
- private final MockBillingController billingController = new MockBillingController(clock, billingDb);
- private final RoleMaintainerMock roleMaintainer = new RoleMaintainerMock();
-
- public ServiceRegistryMock(SystemName system) {
- this.zoneRegistryMock = new ZoneRegistryMock(system);
- this.configServerMock = new ConfigServerMock(zoneRegistryMock, memoryNameService);
- this.mockTesterCloud = new MockTesterCloud(memoryNameService);
- this.clock.setInstant(Instant.ofEpochSecond(1600000000));
- this.controllerVersion = new ControllerVersion(Version.fromString("6.1.0"), "badb01", clock.instant());
- }
-
- @Inject
- public ServiceRegistryMock(ConfigserverConfig config) {
- this(SystemName.from(config.system()));
- }
-
- public ServiceRegistryMock() {
- this(SystemName.main);
- }
-
- @Override
- public ConfigServerMock configServer() {
- return configServerMock;
- }
-
- @Override
- public ManualClock clock() {
- return clock;
- }
-
- @Override
- public ControllerVersion controllerVersion() {
- return controllerVersion;
- }
-
- @Override
- public HostName getHostname() {
- return HostName.of("test-controller");
- }
-
- @Override
- public MockMailer mailer() {
- return mockMailer;
- }
-
- @Override
- public EndpointCertificateProviderMock endpointCertificateProvider() {
- return endpointCertificateProviderMock;
- }
-
- @Override
- public EndpointCertificateValidator endpointCertificateValidator() {
- return endpointCertificateValidatorMock;
- }
-
- @Override
- public MockContactRetriever contactRetriever() {
- return mockContactRetriever;
- }
-
- @Override
- public MockIssueHandler issueHandler() {
- return mockIssueHandler;
- }
-
- @Override
- public DummyOwnershipIssues ownershipIssues() {
- return dummyOwnershipIssues;
- }
-
- @Override
- public LoggingDeploymentIssues deploymentIssues() {
- return loggingDeploymentIssues;
- }
-
- @Override
- public MemoryEntityService entityService() {
- return memoryEntityService;
- }
-
- @Override
- public CostReportConsumerMock costReportConsumer() {
- return costReportConsumerMock;
- }
-
- @Override
- public ArtifactRepositoryMock artifactRepository() {
- return artifactRepositoryMock;
- }
-
- @Override
- public MockTesterCloud testerCloud() {
- return mockTesterCloud;
- }
-
- @Override
- public ApplicationStoreMock applicationStore() {
- return applicationStoreMock;
- }
-
- @Override
- public MockRunDataStore runDataStore() {
- return mockRunDataStore;
- }
-
- @Override
- public MemoryNameService nameService() {
- return memoryNameService;
- }
-
- @Override
- public MockVpcEndpointService vpcEndpointService() {
- return vpcEndpointService;
- }
-
- @Override
- public ZoneRegistryMock zoneRegistry() {
- return zoneRegistryMock;
- }
-
- @Override
- public ConsoleUrls consoleUrls() {
- return consoleUrls;
- }
-
- @Override
- public MockResourceTagger resourceTagger() {
- return mockResourceTagger;
- }
-
- @Override
- public MockEnclaveAccessService enclaveAccessService() {
- return mockAMIService;
- }
-
- @Override
- public MockRoleService roleService() {
- return roleService;
- }
-
- @Override
- public DummySystemMonitor systemMonitor() {
- return systemMonitor;
- }
-
- @Override
- public BillingController billingController() {
- return billingController;
- }
-
- @Override
- public Optional<ArtifactRegistryMock> artifactRegistry(CloudName cloudName) {
- return Optional.of(containerRegistry);
- }
-
- @Override
- public NoopTenantSecretService tenantSecretService() {
- return tenantSecretService;
- }
-
- @Override
- public EndpointSecretManager secretManager() {
- return secretManager;
- }
-
- @Override
- public ArchiveService archiveService() {
- return archiveService;
- }
-
- @Override
- public MockChangeRequestClient changeRequestClient() {
- return changeRequestClient;
- }
-
- @Override
- public AccessControlService accessControlService() {
- return accessControlService;
- }
-
- @Override
- public HorizonClient horizonClient() {
- return horizonClient;
- }
-
- @Override
- public PlanRegistry planRegistry() {
- return planRegistry;
- }
-
- @Override
- public ResourceDatabaseClient resourceDatabase() {
- return resourceDb;
- }
-
- @Override
- public BillingDatabaseClient billingDatabase() {
- return billingDb;
- }
-
- @Override
- public RoleMaintainer roleMaintainer() {
- return roleMaintainer;
- }
-
- public ConfigServerMock configServerMock() {
- return configServerMock;
- }
-
- public MockContactRetriever contactRetrieverMock() {
- return mockContactRetriever;
- }
-
- public EndpointCertificateProviderMock endpointCertificateMock() {
- return endpointCertificateProviderMock;
- }
-
- public RoleMaintainerMock roleMaintainerMock() {
- return roleMaintainer;
- }
-
- public GcpSecretStore gcpSecretStore() { return new NoopGcpSecretStore(); }
-
- @Override
- public BillingReporter billingReporter() {
- return billingReporter;
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneApiMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneApiMock.java
deleted file mode 100644
index e49440976de..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneApiMock.java
+++ /dev/null
@@ -1,143 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-
-import java.util.Objects;
-
-/**
- * @author hakonhall
- */
-public class ZoneApiMock implements ZoneApi {
-
- private final SystemName systemName;
- private final ZoneId id;
- private final ZoneId virtualId;
- private final CloudName cloudName;
- private final String cloudNativeRegionName;
- private final String cloudNativeAvailabilityZone;
-
- public static Builder newBuilder() { return new Builder(); }
-
- private ZoneApiMock(SystemName systemName, ZoneId id, ZoneId virtualId, CloudName cloudName, String cloudNativeRegionName, String cloudNativeAvailabilityZone) {
- this.systemName = systemName;
- this.id = id;
- this.virtualId = virtualId;
- this.cloudName = cloudName;
- this.cloudNativeRegionName = cloudNativeRegionName;
- this.cloudNativeAvailabilityZone = cloudNativeAvailabilityZone;
- if (virtualId != null && virtualId.equals(id)) {
- throw new IllegalArgumentException("Virtual ID cannot be equal to zone ID: " + id);
- }
- }
-
- public static ZoneApiMock fromId(String zoneId) {
- return from(ZoneId.from(zoneId));
- }
-
- public static ZoneApiMock from(Environment environment, RegionName region) {
- return from(ZoneId.from(environment, region));
- }
-
- public static ZoneApiMock from(ZoneId id) {
- return newBuilder().with(id).build();
- }
-
- @Override
- public SystemName getSystemName() { return systemName; }
-
- @Override
- public ZoneId getId() { return id; }
-
- @Override
- public ZoneId getVirtualId() {
- return virtualId == null ? getId() : virtualId;
- }
-
- @Override
- public CloudName getCloudName() { return cloudName; }
-
- @Override
- public String getCloudNativeRegionName() { return cloudNativeRegionName; }
-
- @Override
- public String getCloudNativeAvailabilityZone() { return cloudNativeAvailabilityZone; }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ZoneApiMock that = (ZoneApiMock) o;
- return id.equals(that.id);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(id);
- }
-
- @Override
- public String toString() {
- return id.toString();
- }
-
- public static class Builder {
-
- private SystemName systemName = SystemName.defaultSystem();
- private ZoneId id = ZoneId.defaultId();
- private ZoneId virtualId = null;
- private CloudName cloudName = CloudName.DEFAULT;
- private String cloudNativeRegionName = id.region().value();
- private String cloudNativeAvailabilityZone = "az1";
-
- public Builder with(ZoneId id) {
- this.id = id;
- return this;
- }
-
- public Builder withSystem(SystemName systemName) {
- this.systemName = systemName;
- return this;
- }
-
- public Builder withId(String id) {
- return with(ZoneId.from(id));
- }
-
- public Builder withVirtualId(ZoneId virtualId) {
- this.virtualId = virtualId;
- return this;
- }
-
- public Builder withVirtualId(String virtualId) {
- return withVirtualId(ZoneId.from(virtualId));
- }
-
- public Builder with(CloudName cloudName) {
- this.cloudName = cloudName;
- return this;
- }
-
- public Builder withCloud(String cloud) { return with(CloudName.from(cloud)); }
-
- public Builder withCloudNativeRegionName(String cloudRegionName) {
- this.cloudNativeRegionName = cloudRegionName;
- return this;
- }
-
- public Builder withCloudNativeAvailabilityZone(String cloudAvailabilityZone) {
- this.cloudNativeAvailabilityZone = cloudAvailabilityZone;
- return this;
- }
-
- public ZoneApiMock build() {
- return new ZoneApiMock(systemName, id, virtualId, cloudName, cloudNativeRegionName, cloudNativeAvailabilityZone);
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneFilterMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneFilterMock.java
deleted file mode 100644
index e2b5768bf33..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneFilterMock.java
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneFilter;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.config.provision.zone.ZoneList;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-/**
- * A ZoneList implementation which assumes all zones are controllerManaged.
- *
- * @author jonmv
- */
-public class ZoneFilterMock implements ZoneList {
-
- private final List<ZoneApi> zones;
- private final Map<ZoneApi, RoutingMethod> zoneRoutingMethods;
- private final Set<ZoneApi> dynamicallyProvisioned;
- private final boolean negate;
-
- private ZoneFilterMock(List<ZoneApi> zones, Map<ZoneApi, RoutingMethod> zoneRoutingMethods, Set<ZoneApi> dynamicallyProvisioned, boolean negate) {
- this.zones = zones;
- this.zoneRoutingMethods = zoneRoutingMethods;
- this.dynamicallyProvisioned = dynamicallyProvisioned;
- this.negate = negate;
- }
-
- public static ZoneFilter from(Collection<? extends ZoneApi> zones, Map<ZoneApi, RoutingMethod> routingMethods, Set<ZoneApi> dynamicallyProvisioned) {
- return new ZoneFilterMock(List.copyOf(zones), Map.copyOf(routingMethods), dynamicallyProvisioned, false);
- }
-
- @Override
- public ZoneList not() {
- return new ZoneFilterMock(zones, zoneRoutingMethods, dynamicallyProvisioned, ! negate);
- }
-
- @Override
- public ZoneList all() {
- return filter(zone -> true);
- }
-
- @Override
- public ZoneList publiclyVisible() {
- return controllerUpgraded();
- }
-
- @Override
- public ZoneList controllerUpgraded() {
- return all();
- }
-
- @Override
- public ZoneList routingMethod(RoutingMethod method) {
- return filter(zone -> zoneRoutingMethods.get(zone) == method);
- }
-
- @Override
- public ZoneList reachable() {
- return all();
- }
-
- @Override
- public ZoneList dynamicallyProvisioned() {
- return filter(dynamicallyProvisioned::contains);
- }
-
- @Override
- public ZoneList in(Environment... environments) {
- return filter(zone -> Set.of(environments).contains(zone.getEnvironment()));
- }
-
- @Override
- public ZoneList in(RegionName... regions) {
- return filter(zone -> Set.of(regions).contains(zone.getRegionName()));
- }
-
- @Override
- public ZoneList in(CloudName... clouds) {
- return filter(zone -> Set.of(clouds).contains(zone.getCloudName()));
- }
-
- @Override
- public ZoneList among(ZoneId... zones) {
- return filter(zone -> Set.of(zones).contains(zone.getId()));
- }
-
- @Override
- public List<? extends ZoneApi> zones() {
- return List.copyOf(zones);
- }
-
- private ZoneFilterMock filter(Predicate<ZoneApi> condition) {
- return new ZoneFilterMock(
- zones.stream()
- .filter(zone -> negate ?
- condition.negate().test(zone) :
- condition.test(zone))
- .toList(),
- zoneRoutingMethods, dynamicallyProvisioned, false);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java
deleted file mode 100644
index c5b11fe21b0..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ZoneRegistryMock.java
+++ /dev/null
@@ -1,283 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.integration;
-
-import com.yahoo.component.AbstractComponent;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.UpgradePolicy;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneFilter;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.text.Text;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzService;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.zone.ZoneRegistry;
-
-import java.net.URI;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.NoSuchElementException;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-
-/**
- * @author mpolden
- */
-public class ZoneRegistryMock extends AbstractComponent implements ZoneRegistry {
-
- private final Map<ZoneId, Duration> deploymentTimeToLive = new HashMap<>();
- private final Map<Environment, RegionName> defaultRegionForEnvironment = new HashMap<>();
- private final Map<CloudName, UpgradePolicy> osUpgradePolicies = new HashMap<>();
- private final Map<ZoneApi, RoutingMethod> zoneRoutingMethods = new HashMap<>();
- private final Map<CloudAccount, Set<ZoneId>> cloudAccountZones = new HashMap<>();
- private final Set<ZoneApi> dynamicallyProvisioned = new HashSet<>();
- private final SystemName system; // Don't even think about making it non-final! ƪ(`▿▿▿▿´ƪ)
-
- private List<? extends ZoneApi> zones;
- private CloudAccount systemCloudAccount = CloudAccount.from("111333555777");
- private UpgradePolicy upgradePolicy = null;
-
- /**
- * This sets the default list of zones contained in this. If your test need a particular set of zones, use
- * {@link #setZones(List)} instead of changing the default set.
- */
- public ZoneRegistryMock(SystemName system) {
- this.system = system;
- if (system.isPublic()) {
- this.zones = List.of(ZoneApiMock.newBuilder().withId("test.us-east-1").withCloud("aws").withCloudNativeAvailabilityZone("use1-az4").build(),
- ZoneApiMock.newBuilder().withId("staging.us-east-3").withCloud("aws").withCloudNativeAvailabilityZone("use3-az1").build(),
- ZoneApiMock.newBuilder().withId("prod.aws-us-east-1c").withCloud("aws").withCloudNativeAvailabilityZone("use1-az2").build(),
- ZoneApiMock.newBuilder().withId("prod.aws-eu-west-1a").withCloud("aws").withCloudNativeAvailabilityZone("euw1-az3").build(),
- ZoneApiMock.newBuilder().withId("dev.aws-us-east-1c").withCloud("aws").withCloudNativeAvailabilityZone("use1-az2").build());
- setRoutingMethod(this.zones, RoutingMethod.exclusive);
- } else {
- this.zones = List.of(ZoneApiMock.fromId("test.us-east-1"),
- ZoneApiMock.fromId("staging.us-east-3"),
- ZoneApiMock.fromId("dev.us-east-1"),
- ZoneApiMock.newBuilder().withId("dev.aws-us-east-2a").withCloud("aws").build(),
- ZoneApiMock.fromId("perf.us-east-3"),
- ZoneApiMock.newBuilder().withId("prod.aws-us-east-1a").withCloud("aws").withCloudNativeRegionName("us-east-1").build(),
- ZoneApiMock.newBuilder().withId("prod.aws-us-east-1b").withCloud("aws").withCloudNativeRegionName("us-east-1").build(),
- ZoneApiMock.fromId("prod.ap-northeast-1"),
- ZoneApiMock.fromId("prod.ap-northeast-2"),
- ZoneApiMock.fromId("prod.ap-southeast-1"),
- ZoneApiMock.fromId("prod.us-east-3"),
- ZoneApiMock.fromId("prod.us-west-1"),
- ZoneApiMock.fromId("prod.us-central-1"),
- ZoneApiMock.fromId("prod.eu-west-1"));
- for (ZoneApi zone : this.zones)
- setRoutingMethod(zone, zone.getCloudName().equals(CloudName.DEFAULT) ? RoutingMethod.sharedLayer4 : RoutingMethod.exclusive);
- }
- }
-
- public ZoneRegistryMock setDeploymentTimeToLive(ZoneId zone, Duration duration) {
- deploymentTimeToLive.put(zone, duration);
- return this;
- }
-
- public ZoneRegistryMock setDefaultRegionForEnvironment(Environment environment, RegionName region) {
- defaultRegionForEnvironment.put(environment, region);
- return this;
- }
-
- public ZoneRegistryMock setZones(List<? extends ZoneApi> zones) {
- this.zones = zones;
- return this;
- }
-
- public ZoneRegistryMock setZones(ZoneApi... zone) {
- return setZones(List.of(zone));
- }
-
- public ZoneRegistryMock addZones(ZoneApi... zones) {
- List<ZoneApi> allZones = new ArrayList<>(this.zones);
- Collections.addAll(allZones, zones);
- return setZones(allZones);
- }
-
- public ZoneRegistryMock setUpgradePolicy(UpgradePolicy upgradePolicy) {
- this.upgradePolicy = upgradePolicy;
- return this;
- }
-
- public ZoneRegistryMock setOsUpgradePolicy(CloudName cloud, UpgradePolicy upgradePolicy) {
- osUpgradePolicies.put(cloud, upgradePolicy);
- return this;
- }
-
- public ZoneRegistryMock exclusiveRoutingIn(ZoneApi... zones) {
- return exclusiveRoutingIn(List.of(zones));
- }
-
- public ZoneRegistryMock exclusiveRoutingIn(List<? extends ZoneApi> zones) {
- return setRoutingMethod(zones, RoutingMethod.exclusive);
- }
-
- public ZoneRegistryMock setRoutingMethod(List<? extends ZoneApi> zones, RoutingMethod routingMethod) {
- zones.forEach(zone -> setRoutingMethod(zone, routingMethod));
- return this;
- }
-
- public ZoneRegistryMock setRoutingMethod(ZoneApi zone, RoutingMethod routingMethod) {
- this.zoneRoutingMethods.put(zone, routingMethod);
- return this;
- }
-
- public ZoneRegistryMock dynamicProvisioningIn(ZoneApi... zones) {
- return dynamicProvisioningIn(List.of(zones));
- }
-
- public ZoneRegistryMock dynamicProvisioningIn(List<ZoneApi> zones) {
- this.dynamicallyProvisioned.addAll(zones);
- return this;
- }
-
- public ZoneRegistryMock configureCloudAccount(CloudAccount cloudAccount, ZoneId... zones) {
- this.cloudAccountZones.computeIfAbsent(cloudAccount, (k) -> new HashSet<>()).addAll(Set.of(zones));
- return this;
- }
-
- @Override
- public SystemName system() {
- return system;
- }
-
- @Override
- public ZoneApi systemZone() {
- return ZoneApiMock.newBuilder().withSystem(system).withVirtualId(ZoneId.ofVirtualControllerZone()).build();
- }
-
- @Override
- public ZoneFilter zones() {
- return ZoneFilterMock.from(zones, zoneRoutingMethods, dynamicallyProvisioned);
- }
-
- @Override
- public ZoneFilter zonesIncludingSystem() {
- var fullZones = new ArrayList<ZoneApi>(1 + zones.size());
- fullZones.add(systemAsZone());
- fullZones.addAll(zones);
- return ZoneFilterMock.from(fullZones, zoneRoutingMethods, dynamicallyProvisioned);
- }
-
- private ZoneApiMock systemAsZone() {
- return ZoneApiMock.newBuilder()
- .with(ZoneId.from("prod.us-east-1"))
- .withVirtualId(ZoneId.ofVirtualControllerZone())
- .build();
- }
-
- @Override
- public AthenzService getConfigServerHttpsIdentity(ZoneId zone) {
- return new AthenzService("vespadomain", "provider-" + zone.environment().value() + "-" + zone.region().value());
- }
-
- @Override
- public AthenzIdentity getNodeAthenzIdentity(ZoneId zoneId, NodeType nodeType) {
- return new AthenzService("vespadomain", "servicename");
- }
-
- @Override
- public AthenzDomain accessControlDomain() {
- return AthenzDomain.from("vespadomain");
- }
-
- @Override
- public UpgradePolicy upgradePolicy() {
- return upgradePolicy;
- }
-
- @Override
- public UpgradePolicy osUpgradePolicy(CloudName cloud) {
- return osUpgradePolicies.get(cloud);
- }
-
- @Override
- public List<UpgradePolicy> osUpgradePolicies() {
- return List.copyOf(osUpgradePolicies.values());
- }
-
- @Override
- public RoutingMethod routingMethod(ZoneId zone) {
- return Objects.requireNonNull(zoneRoutingMethods.get(ZoneApiMock.from(zone)));
- }
-
- @Override
- public URI apiUrl() {
- return URI.create("https://api.tld:4443/");
- }
-
- @Override public Optional<String> tenantDeveloperRoleArn(TenantName tenant) { return Optional.empty(); }
-
- @Override
- public Optional<AthenzDomain> cloudAccountAthenzDomain(CloudAccount cloudAccount) {
- return Optional.of(AthenzDomain.from("vespa.enclave"));
- }
-
- @Override
- public boolean hasZone(ZoneId zoneId) {
- return zones.stream().anyMatch(zone -> zone.getId().equals(zoneId));
- }
-
- @Override
- public boolean hasZone(ZoneId zoneId, CloudAccount cloudAccount) {
- return hasZone(zoneId) && (system.isPublic() || cloudAccountZones.getOrDefault(cloudAccount, Set.of()).contains(zoneId));
- }
-
- @Override
- public boolean isExternal(CloudAccount cloudAccount) {
- return system.isPublic() && !cloudAccount.isUnspecified() && !cloudAccount.equals(systemCloudAccount);
- }
-
- @Override
- public URI getConfigServerVipUri(ZoneId zoneId) {
- return URI.create(Text.format("https://cfg.%s.test.vip:4443/", zoneId.value()));
- }
-
- @Override
- public Optional<String> getVipHostname(ZoneId zoneId) {
- if (routingMethod(zoneId).isShared()) {
- return Optional.of("vip." + zoneId.value());
- }
- return Optional.empty();
- }
-
- @Override
- public Optional<Duration> getDeploymentTimeToLive(ZoneId zoneId) {
- return Optional.ofNullable(deploymentTimeToLive.get(zoneId));
- }
-
- @Override
- public Optional<RegionName> getDefaultRegion(Environment environment) {
- return Optional.ofNullable(defaultRegionForEnvironment.get(environment));
- }
-
- @Override
- public ZoneApi get(ZoneId zoneId) {
- return zones.stream()
- .filter(zone -> zone.getId().equals(zoneId))
- .findFirst()
- .orElseThrow(() -> new NoSuchElementException("No zone with id '" + zoneId + "'"));
- }
-
- @Override
- public URI getMonitoringSystemUri(DeploymentId deploymentId) {
- return URI.create("http://monitoring-system.test/?environment=" + deploymentId.zoneId().environment().value() + "&region="
- + deploymentId.zoneId().region().value() + "&application=" + deploymentId.applicationId().toShortString());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java
deleted file mode 100644
index 8aaf1e2a928..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ApplicationOwnershipConfirmerTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.AccountId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.ApplicationSummary;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.OwnershipIssues;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Optional;
-
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTester.appId;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author jonmv
- */
-public class ApplicationOwnershipConfirmerTest {
-
- private MockOwnershipIssues issues;
- private ApplicationOwnershipConfirmer confirmer;
- private DeploymentTester tester;
-
- @BeforeEach
- public void setup() {
- tester = new DeploymentTester();
- issues = new MockOwnershipIssues();
- confirmer = new ApplicationOwnershipConfirmer(tester.controller(), Duration.ofDays(1), issues, 1);
- }
-
- @Test
- void testConfirmation() {
- Optional<Contact> contact = Optional.of(tester.controllerTester().serviceRegistry().contactRetrieverMock().contact());
- var app = tester.newDeploymentContext();
- tester.controller().tenants().lockOrThrow(appId.tenant(), LockedTenant.Athenz.class, tenant ->
- tester.controller().tenants().store(tenant.with(contact.get())));
- app.submit().deploy();
-
- var appWithoutContact = tester.newDeploymentContext("other", "application", "default");
- appWithoutContact.submit().deploy();
-
- assertFalse(app.application().ownershipIssueId().isPresent(), "No issue is initially stored for a new application.");
- assertFalse(appWithoutContact.application().ownershipIssueId().isPresent(), "No issue is initially stored for a new application.");
- assertFalse(issues.escalated, "No escalation has been attempted for a new application");
-
- // Set response from the issue mock, which will be obtained by the maintainer on issue filing.
- Optional<IssueId> issueId = Optional.of(IssueId.from("1"));
- issues.response = issueId;
- confirmer.maintain();
-
- assertFalse(app.application().ownershipIssueId().isPresent(), "No issue is stored for an application newer than 3 months.");
- assertFalse(appWithoutContact.application().ownershipIssueId().isPresent(), "No issue is stored for an application newer than 3 months.");
-
- tester.clock().advance(Duration.ofDays(91));
- confirmer.maintain();
-
- assertEquals(issueId, app.application().ownershipIssueId(), "Confirmation issue has been filed for application with contact.");
- assertTrue(issues.escalated, "The confirmation issue response has been ensured.");
- assertEquals(Optional.empty(), appWithoutContact.application().ownershipIssueId(), "No confirmation issue has been filed for application without contact.");
-
- // No new issue is created, so return empty now.
- issues.response = Optional.empty();
- confirmer.maintain();
-
- assertEquals(issueId, app.application().ownershipIssueId(), "Confirmation issue reference is not updated when no issue id is returned.");
-
- // Time has passed, and a new confirmation issue is in order for the property which is still in production.
- Optional<IssueId> issueId2 = Optional.of(IssueId.from("2"));
- issues.response = issueId2;
- confirmer.maintain();
-
- assertEquals(issueId2, app.application().ownershipIssueId(), "A new confirmation issue id is stored when something is returned to the maintainer.");
-
- assertFalse(app.application().issueOwner().isPresent(), "No owner is stored for application");
- issues.owner = Optional.of(new AccountId("username"));
- confirmer.maintain();
- assertEquals(app.application().issueOwner().get().value(), "username", "Owner has been added to application");
-
- // The app deletes all production deployments — see that the issue is forgotten.
- assertEquals(issueId2, app.application().ownershipIssueId(), "Confirmation issue for application is still open.");
- app.application().productionDeployments().values().stream().flatMap(List::stream)
- .forEach(deployment -> tester.controller().applications().deactivate(app.instanceId(), deployment.zone()));
- assertTrue(app.application().require(InstanceName.defaultName()).productionDeployments().isEmpty(), "No production deployments are listed for user.");
- confirmer.maintain();
-
- // Time has passed, and a new confirmation issue is in order for the property which is still in production.
- issues.response = Optional.of(IssueId.from("3"));
- confirmer.maintain();
-
- assertEquals(issueId2, app.application().ownershipIssueId(), "Confirmation issue for application without production deployments has not been filed.");
- }
-
- private static class MockOwnershipIssues implements OwnershipIssues {
-
- private Optional<IssueId> response;
- private boolean escalated = false;
- private Optional<AccountId> owner = Optional.empty();
-
- @Override
- public Optional<IssueId> confirmOwnership(Optional<IssueId> issueId, ApplicationSummary summary, AccountId assigneeId, User assignee, Contact contact) {
- return response;
- }
-
- @Override
- public void ensureResponse(IssueId issueId, Optional<Contact> contact) {
- escalated = true;
- }
-
- @Override
- public Optional<AccountId> getConfirmedOwner(IssueId issueId) {
- return owner;
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java
deleted file mode 100644
index 0b826e8f375..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveAccessMaintainerTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.jdisc.test.MockMetric;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.MockArchiveService;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author andreer
- */
-public class ArchiveAccessMaintainerTest {
-
- @Test
- void grantsRoleAccess() {
- var tester = new ControllerTester(SystemName.Public);
-
- String tenant1role = "arn:aws:iam::123456789012:role/my-role";
- String tenant2role = "arn:aws:iam::210987654321:role/my-role";
- var tenant1 = createTenantWithAccessRole(tester, "tenant1", tenant1role);
- var tenant2 = createTenantWithAccessRole(tester, "tenant2", tenant2role);
-
- ZoneId testZone = ZoneId.from("prod.aws-us-east-1c");
- tester.controller().archiveBucketDb().archiveUriFor(testZone, tenant1, true);
-
- MockArchiveService archiveService = (MockArchiveService) tester.controller().serviceRegistry().archiveService();
-
- assertEquals(0, archiveService.authorizeAccessByTenantName.size());
- MockMetric metric = new MockMetric();
- new ArchiveAccessMaintainer(tester.controller(), metric, Duration.ofMinutes(10)).maintain();
- assertEquals(new ArchiveAccess().withAWSRole(tenant1role), archiveService.authorizeAccessByTenantName.get(tenant1));
- assertEquals(new ArchiveAccess().withAWSRole(tenant2role), archiveService.authorizeAccessByTenantName.get(tenant2));
-
- var zoneRegistry = tester.controller().zoneRegistry();
- var expected = Map.of("archive.bucketCount",
- zoneRegistry.zonesIncludingSystem().all().ids().stream()
- .collect(Collectors.toMap(
- zone -> Map.of("zone", zone.value(), "cloud",
- zoneRegistry.hasZone(zone) ? zoneRegistry.get(zone).getCloudName().value() : "default"),
- zone -> zone.equals(testZone) ? 1d : 0d)));
-
- assertEquals(expected, metric.metrics());
- }
-
- private TenantName createTenantWithAccessRole(ControllerTester tester, String tenantName, String role) {
- var tenant = tester.createTenant(tenantName, Tenant.Type.cloud);
- tester.controller().tenants().lockOrThrow(tenant, LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withArchiveAccess(new ArchiveAccess().withAWSRole(role));
- tester.controller().tenants().store(lockedTenant);
- });
- return tenant;
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java
deleted file mode 100644
index 0a388806146..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArchiveUriUpdaterTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveUriUpdate;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.MockArchiveService;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.VespaManagedArchiveBucket;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ArchiveUris;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeRepository;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author freva
- */
-public class ArchiveUriUpdaterTest {
-
- private final DeploymentTester tester = new DeploymentTester(new ControllerTester(SystemName.Public));
-
- @Test
- void archive_uri_test() {
- var updater = new ArchiveUriUpdater(tester.controller(), Duration.ofDays(1));
-
- var tenant1 = TenantName.from("tenant1");
- var tenant2 = TenantName.from("tenant2");
- var account1 = CloudAccount.from("001122334455");
- var tenantInfra = SystemApplication.TENANT;
- ZoneId zone = ZoneId.from("prod", "aws-us-east-1c");
-
- // Initially we should only is the bucket for hosted-vespa tenant
- updater.maintain();
- assertArchiveUris(zone, Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/"), Map.of());
- assertArchiveUris(ZoneId.from("prod", "controller"), Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/"), Map.of());
-
- // Archive service now has URI for tenant1, but tenant1 is not deployed in zone
- setBucketNameInService(Map.of(tenant2, "uri-1"), zone);
- setAccountBucketNameInService(zone, account1, "bkt-1");
- updater.maintain();
- assertArchiveUris(zone, Map.of(TenantName.from("hosted-vespa"), "s3://bucketName/"), Map.of());
-
- ((InMemoryFlagSource) tester.controller().flagSource())
- .withListFlag(PermanentFlags.CLOUD_ACCOUNTS.id(), List.of(account1.value()), String.class);
- deploy(tester.newDeploymentContext(tenant1.value(), "app1", "instance1"), zone, account1);
- deploy(tester.newDeploymentContext(tenant2.value(), "app1", "instance1"), zone, CloudAccount.empty);
-
- updater.maintain();
- assertArchiveUris(zone, Map.of(tenant2, "s3://uri-1/", tenantInfra, "s3://bucketName/"), Map.of(account1, "s3://bkt-1/"));
-
- // URI for tenant1 should be updated and removed for tenant2
- setArchiveUriInNodeRepo(Map.of(tenant1, "wrong-uri", tenant2, "uri-2"), zone);
- updater.maintain();
- assertArchiveUris(zone, Map.of(tenant2, "s3://uri-1/", tenantInfra, "s3://bucketName/"), Map.of(account1, "s3://bkt-1/"));
- }
-
- private void assertArchiveUris(ZoneId zone, Map<TenantName, String> expectedTenantUris, Map<CloudAccount, String> expectedAccountUris) {
- ArchiveUris archiveUris = tester.controller().serviceRegistry().configServer().nodeRepository().getArchiveUris(zone);
- assertEquals(expectedTenantUris, archiveUris.tenantArchiveUris().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())));
- assertEquals(expectedAccountUris, archiveUris.accountArchiveUris().entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().toString())));
- }
-
- private void setBucketNameInService(Map<TenantName, String> bucketNames, ZoneId zone) {
- ArchiveBuckets buckets = tester.controller().curator().readArchiveBuckets(zone);
- for (var entry : bucketNames.entrySet())
- buckets = buckets.with(new VespaManagedArchiveBucket(entry.getValue(), "keyArn").withTenant(entry.getKey()));
- tester.controller().curator().writeArchiveBuckets(zone, buckets);
- }
-
- private void setAccountBucketNameInService(ZoneId zone, CloudAccount cloudAccount, String bucketName) {
- ((MockArchiveService) tester.controller().serviceRegistry().archiveService()).setEnclaveArchiveBucket(zone, cloudAccount, bucketName);
- }
-
- private void setArchiveUriInNodeRepo(Map<TenantName, String> archiveUris, ZoneId zone) {
- NodeRepository nodeRepository = tester.controller().serviceRegistry().configServer().nodeRepository();
- archiveUris.forEach((tenant, uri) -> nodeRepository.updateArchiveUri(zone, ArchiveUriUpdate.setArchiveUriFor(tenant, URI.create(uri))));
- }
-
- private void deploy(DeploymentContext application, ZoneId zone, CloudAccount cloudAccount) {
- application.submit(new ApplicationPackageBuilder()
- .cloudAccount(cloudAccount.value())
- .region(zone.region().value())
- .build()).deploy();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArtifactExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArtifactExpirerTest.java
deleted file mode 100644
index 17233496e31..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ArtifactExpirerTest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.artifact.Artifact;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.integration.ArtifactRegistryMock;
-import org.junit.jupiter.api.Test;
-
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class ArtifactExpirerTest {
-
- private static final Path configModelPath = Paths.get("src/test/resources/config-models/");
-
- @Test
- void maintain() {
- DeploymentTester tester = new DeploymentTester();
- // Note: No models in config-models-*.xml
- ArtifactExpirer expirer = new ArtifactExpirer(tester.controller(), Duration.ofDays(1), configModelPath.resolve("empty"));
- ArtifactRegistryMock registry = tester.controllerTester().serviceRegistry().artifactRegistry(CloudName.DEFAULT).orElseThrow();
-
- Instant instant = tester.clock().instant();
- Artifact image0 = new Artifact("image0", "registry.example.com", "vespa/vespa", "7.1", instant, Version.fromString("7.1"));
- Artifact image1 = new Artifact("image1", "registry.example.com", "vespa/vespa", "7.2.42-amd64", instant, Version.fromString("7.2.42"));
- Artifact image2 = new Artifact("image2", "registry.example.com", "vespa/vespa", "7.2.42.a-amd64", instant, Version.fromString("7.2.42.a"));
- Artifact image3 = new Artifact("image3", "registry.example.com", "vespa/vespa", "7.4-amd64", instant, Version.fromString("7.4"));
- registry.add(image0)
- .add(image1)
- .add(image2)
- .add(image3);
-
- // Make one image active
- tester.controllerTester().upgradeSystem(image1.version());
-
- // Nothing is expired initially
- expirer.maintain();
- assertEquals(List.of(image0, image1, image2, image3), registry.list());
-
- // Nothing is expired as not enough time has passed since image creation
- tester.clock().advance(Duration.ofDays(1));
- expirer.maintain();
- assertEquals(List.of(image0, image1, image2, image3), registry.list());
-
- // Enough time passes to expire unused image
- tester.clock().advance(Duration.ofDays(13).plus(Duration.ofSeconds(1)));
- expirer.maintain();
- assertEquals(List.of(image1, image2, image3), registry.list());
-
- // A new version is published and controllers upgrade. This version, the system version + its unofficial
- // version and future versions are all kept
- Artifact image4 = new Artifact("image4", "registry.example.com", "vespa/vespa", "7.3.0-arm64", tester.clock().instant(), Version.fromString("7.3.0"));
- registry.add(image4);
- tester.controllerTester().upgradeController(image4.version());
- expirer.maintain();
- assertEquals(List.of(image1, image2, image4, image3), registry.list());
-
- // The system upgrades, only the active and future version are kept
- tester.controllerTester().upgradeSystem(image4.version());
- expirer.maintain();
- assertEquals(List.of(image4, image3), registry.list());
- }
-
- @Test
- void maintainWithConfigModelsInUse() {
- DeploymentTester tester = new DeploymentTester(new ControllerTester(SystemName.cd));
- ArtifactExpirer expirer = new ArtifactExpirer(tester.controller(), Duration.ofDays(1), configModelPath.resolve("cd"));
- ArtifactRegistryMock registry = tester.controllerTester().serviceRegistry().artifactRegistry(CloudName.DEFAULT).orElseThrow();
-
- Instant instant = tester.clock().instant();
- // image0 (with version 8.210.1) is not present in config-models-*.xml
- Artifact image0 = new Artifact("image0", "registry.example.com", "vespa/vespa", "8.210.1", instant, Version.fromString("8.210.1"));
- Artifact image1 = new Artifact("image1", "registry.example.com", "vespa/vespa", "8.220.15", instant, Version.fromString("8.220.15"));
- Artifact image2 = new Artifact("image2", "registry.example.com", "vespa/vespa", "8.223.1", instant, Version.fromString("8.223.1"));
-
- registry.add(image0)
- .add(image1)
- .add(image2);
-
- // Make one image active
- tester.controllerTester().upgradeSystem(image1.version());
-
- // Nothing is expired initially, image2 is not active, but version is one of known config model versions
- expirer.maintain();
- assertEquals(List.of(image0, image1, image2), registry.list());
-
- // Nothing is expired as not enough time has passed since image creation
- tester.clock().advance(Duration.ofDays(1));
- expirer.maintain();
- assertEquals(List.of(image0, image1, image2), registry.list());
-
- // Enough time passes to expire unused image
- tester.clock().advance(Duration.ofDays(13).plus(Duration.ofSeconds(1)));
- expirer.maintain();
- assertEquals(List.of(image1, image2), registry.list());
-
- // A new version is published and controllers upgrade. This version, the system version + its unofficial
- // version and future versions are all kept
- Artifact image4 = new Artifact("image4", "registry.example.com", "vespa/vespa", "8.223.2-arm64", tester.clock().instant(), Version.fromString("8.223.2"));
- registry.add(image4);
- tester.controllerTester().upgradeController(image4.version());
- expirer.maintain();
- assertEquals(List.of(image1, image2, image4), registry.list());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdaterTest.java
deleted file mode 100644
index bac89b1988c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BcpGroupUpdaterTest.java
+++ /dev/null
@@ -1,316 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterResources;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.IntRange;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Application;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Cluster;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Load;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock;
-import org.junit.jupiter.api.Test;
-
-import java.time.Clock;
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-
-/**
- * Tests the traffic fraction updater. This also tests its dependency on DeploymentMetricsMaintainer.
- *
- * @author bratseth
- */
-public class BcpGroupUpdaterTest {
-
- @Test
- void testTrafficUpdaterImplicitBcp() {
- DeploymentTester tester = new DeploymentTester();
- Version version = Version.fromString("7.1");
- tester.controllerTester().upgradeSystem(Version.fromString("7.1"));
- var context = tester.newDeploymentContext();
- var deploymentMetricsMaintainer = new DeploymentMetricsMaintainer(tester.controller(), Duration.ofDays(1));
- var updater = new BcpGroupUpdater(tester.controller(), Duration.ofDays(1));
- ZoneId prod1 = ZoneId.from("prod", "ap-northeast-1");
- ZoneId prod2 = ZoneId.from("prod", "us-east-3");
- ZoneId prod3 = ZoneId.from("prod", "us-west-1");
- context.runJob(DeploymentContext.perfUsEast3, new ApplicationPackage(new byte[0]), version); // Ignored
- context.runJob(DeploymentContext.productionApNortheast1, new ApplicationPackage(new byte[0]), version);
-
- // One zone
- context.runJob(DeploymentContext.productionApNortheast1, new ApplicationPackage(new byte[0]), version);
- setQpsMetric(50.0, context.application().id().defaultInstance(), prod1, tester);
- setBcpMetrics(1.5, 0.1, 0.45, context.instanceId(), prod1, "cluster1", tester);
- deploymentMetricsMaintainer.maintain();
- assertEquals(0.0, updater.maintain(), 0.0000001);
- assertTrafficFraction(1.0, 1.0, context.instanceId(), prod1, tester);
- assertNoBcpGroupInfo(context.instanceId(), prod1, "cluster1", tester, "No other regions in group");
-
- // Two zones
- context.runJob(DeploymentContext.productionUsEast3, new ApplicationPackage(new byte[0]), version);
- setQpsMetric(60.0, context.application().id().defaultInstance(), prod1, tester);
- setQpsMetric(20.0, context.application().id().defaultInstance(), prod2, tester);
- setBcpMetrics(100.0, 0.1, 0.45, context.instanceId(), prod1, "cluster1", tester);
- deploymentMetricsMaintainer.maintain();
- assertEquals(0.0, updater.maintain(), 0.0000001);
- assertTrafficFraction(0.75, 1.0, context.instanceId(), prod1, tester);
- assertTrafficFraction(0.25, 1.0, context.instanceId(), prod2, tester);
- assertNoBcpGroupInfo(context.instanceId(), prod1, "cluster1", tester,
- "Have no values from the other region (prod2) yet");
- assertBcpGroupInfo(100.0, 0.1, 0.45,
- context.instanceId(), prod2, "cluster1", tester);
- setBcpMetrics(50.0, 0.2, 0.5, context.instanceId(), prod2, "cluster1", tester);
- assertEquals(0.0, updater.maintain(), 0.0000001);
- assertBcpGroupInfo(50.0, 0.2, 0.5,
- context.instanceId(), prod1, "cluster1", tester);
-
- // Three zones
- context.runJob(DeploymentContext.productionUsWest1, new ApplicationPackage(new byte[0]), version);
- setQpsMetric(53.0, context.application().id().defaultInstance(), prod1, tester);
- setQpsMetric(45.0, context.application().id().defaultInstance(), prod2, tester);
- setQpsMetric(02.0, context.application().id().defaultInstance(), prod3, tester);
- deploymentMetricsMaintainer.maintain();
- assertEquals(0.0, updater.maintain(), 0.0000001);
- assertTrafficFraction(0.53, 0.53 + (double)45/2 / 100, context.instanceId(), prod1, tester);
- assertTrafficFraction(0.45, 0.45 + (double)53/2 / 100, context.instanceId(), prod2, tester);
- assertTrafficFraction(0.02, 0.02 + (double)53/2 / 100, context.instanceId(), prod3, tester);
- }
-
- @Test
- void testTrafficUpdaterHotCold() {
- var spec = """
- <deployment version="1.0">
- <staging/>
- <prod>
- <region>ap-northeast-1</region>
- <region>ap-southeast-1</region>
- <region>us-east-3</region>
- <region>us-central-1</region>
- <region>eu-west-1</region>
- </prod>
- <bcp>
- <group>
- <region>ap-northeast-1</region>
- <region>ap-southeast-1</region>
- </group>
- <group>
- <region>us-east-3</region>
- <region>us-central-1</region>
- </group>
- <group>
- <region>eu-west-1</region>
- </group>
- </bcp>
- </deployment>
- """;
-
- DeploymentTester tester = new DeploymentTester();
- Version version = Version.fromString("7.1");
- tester.controllerTester().upgradeSystem(Version.fromString("7.1"));
- var context = tester.newDeploymentContext();
- var deploymentSpec = new DeploymentSpecXmlReader(true).read(spec);
- tester.controller().applications()
- .lockApplicationOrThrow(context.application().id(),
- locked -> tester.controller().applications().store(locked.with(deploymentSpec)));
-
- var deploymentMetricsMaintainer = new DeploymentMetricsMaintainer(tester.controller(), Duration.ofDays(1));
- var updater = new BcpGroupUpdater(tester.controller(), Duration.ofDays(1));
-
- ZoneId ap1 = ZoneId.from("prod", "ap-northeast-1");
- ZoneId ap2 = ZoneId.from("prod", "ap-southeast-1");
- ZoneId us1 = ZoneId.from("prod", "us-east-3");
- ZoneId us2 = ZoneId.from("prod", "us-central-1");
- ZoneId eu1 = ZoneId.from("prod", "eu-west-1");
-
- context.runJob(DeploymentContext.productionApNortheast1, new ApplicationPackage(new byte[0]), version);
- context.runJob(DeploymentContext.productionApSoutheast1, new ApplicationPackage(new byte[0]), version);
- context.runJob(DeploymentContext.productionUsEast3, new ApplicationPackage(new byte[0]), version);
- context.runJob(DeploymentContext.productionUsCentral1, new ApplicationPackage(new byte[0]), version);
- context.runJob(DeploymentContext.productionEuWest1, new ApplicationPackage(new byte[0]), version);
-
- setQpsMetric(50.0, context.application().id().defaultInstance(), ap1, tester);
- setQpsMetric(00.0, context.application().id().defaultInstance(), ap2, tester);
- setQpsMetric(10.0, context.application().id().defaultInstance(), us1, tester);
- setQpsMetric(00.0, context.application().id().defaultInstance(), us2, tester);
- setQpsMetric(40.0, context.application().id().defaultInstance(), eu1, tester);
-
- deploymentMetricsMaintainer.maintain();
- assertEquals(0.0, updater.maintain(), 0.0000001);
- assertTrafficFraction(0.5, 0.5, context.instanceId(), ap1, tester);
- assertTrafficFraction(0.0, 0.5, context.instanceId(), ap2, tester);
- assertTrafficFraction(0.1, 0.1, context.instanceId(), us1, tester);
- assertTrafficFraction(0.0, 0.1, context.instanceId(), us2, tester);
- assertTrafficFraction(0.4, 0.4, context.instanceId(), eu1, tester);
- }
-
- @Test
- void testTrafficUpdaterOverlappingGroups() {
- var spec = """
- <deployment version="1.0">
- <staging/>
- <prod>
- <region>ap-northeast-1</region>
- <region>ap-southeast-1</region>
- <region>us-east-3</region>
- <region>us-central-1</region>
- <region>us-west-1</region>
- <region>eu-west-1</region>
- </prod>
- <bcp>
- <group>
- <region>ap-northeast-1</region>
- <region>ap-southeast-1</region>
- <region fraction="0.5">eu-west-1</region>
- </group>
- <group>
- <region>us-east-3</region>
- <region>us-central-1</region>
- <region>us-west-1</region>
- <region fraction="0.5">eu-west-1</region>
- </group>
- </bcp>
- </deployment>
- """;
-
- DeploymentTester tester = new DeploymentTester();
- Version version = Version.fromString("7.1");
- tester.controllerTester().upgradeSystem(Version.fromString("7.1"));
- var context = tester.newDeploymentContext();
- var deploymentSpec = new DeploymentSpecXmlReader(true).read(spec);
- tester.controller().applications()
- .lockApplicationOrThrow(context.application().id(),
- locked -> tester.controller().applications().store(locked.with(deploymentSpec)));
-
- var deploymentMetricsMaintainer = new DeploymentMetricsMaintainer(tester.controller(), Duration.ofDays(1));
- var updater = new BcpGroupUpdater(tester.controller(), Duration.ofDays(1));
-
- ZoneId ap1 = ZoneId.from("prod", "ap-northeast-1");
- ZoneId ap2 = ZoneId.from("prod", "ap-southeast-1");
- ZoneId us1 = ZoneId.from("prod", "us-east-3");
- ZoneId us2 = ZoneId.from("prod", "us-central-1");
- ZoneId us3 = ZoneId.from("prod", "us-west-1");
- ZoneId eu1 = ZoneId.from("prod", "eu-west-1");
-
- context.runJob(DeploymentContext.productionApNortheast1, new ApplicationPackage(new byte[0]), version);
- context.runJob(DeploymentContext.productionApSoutheast1, new ApplicationPackage(new byte[0]), version);
- context.runJob(DeploymentContext.productionUsEast3, new ApplicationPackage(new byte[0]), version);
- context.runJob(DeploymentContext.productionUsCentral1, new ApplicationPackage(new byte[0]), version);
- context.runJob(DeploymentContext.productionUsWest1, new ApplicationPackage(new byte[0]), version);
- context.runJob(DeploymentContext.productionEuWest1, new ApplicationPackage(new byte[0]), version);
-
- setQpsMetric(20.0, context.application().id().defaultInstance(), ap1, tester);
- setQpsMetric(50.0, context.application().id().defaultInstance(), ap2, tester);
- setQpsMetric(00.0, context.application().id().defaultInstance(), us1, tester);
- setQpsMetric(30.0, context.application().id().defaultInstance(), us2, tester);
- setQpsMetric(40.0, context.application().id().defaultInstance(), us3, tester);
- setQpsMetric(60.0, context.application().id().defaultInstance(), eu1, tester);
-
- deploymentMetricsMaintainer.maintain();
- assertEquals(0.0, updater.maintain(), 0.0000001);
- assertTrafficFraction(0.10, 0.10 + 50 / 200.0 / 1.5, context.instanceId(), ap1, tester);
- assertTrafficFraction(0.25, 0.25 + 30 / 200.0 / 1.5, context.instanceId(), ap2, tester);
- assertTrafficFraction(0.00, 0.00 + 40 / 200.0 / 2.5, context.instanceId(), us1, tester);
- assertTrafficFraction(0.15, 0.15 + 40 / 200.0 / 2.5, context.instanceId(), us2, tester);
- assertTrafficFraction(0.20, 0.20 + 30 / 200.0 / 2.5, context.instanceId(), us3, tester);
- assertTrafficFraction(0.30, 0.30 + 0.5 * 50 / 200.0 / 1.5 + 0.5 * 40 / 200.0 / 2.5, context.instanceId(), eu1, tester);
-
- // BCP group info (missing ap* regions for cluster1, and full for cluster2)
- setBcpMetrics(100, 0.1, 0.1, context.instanceId(), us1, "cluster1", tester);
- setBcpMetrics(100, 0.1, 0.1, context.instanceId(), us2, "cluster1", tester);
- setBcpMetrics(300, 0.3, 0.3, context.instanceId(), us3, "cluster1", tester);
- setBcpMetrics(100, 0.1, 0.1, context.instanceId(), eu1, "cluster1", tester);
-
- setBcpMetrics(100, 0.1, 0.1, context.instanceId(), ap1, "cluster2", tester);
- setBcpMetrics(200, 0.2, 0.2, context.instanceId(), ap2, "cluster2", tester);
- setBcpMetrics(100, 0.1, 0.1, context.instanceId(), us1, "cluster2", tester);
- setBcpMetrics(100, 0.1, 0.1, context.instanceId(), us2, "cluster2", tester);
- setBcpMetrics(300, 0.3, 0.3, context.instanceId(), us3, "cluster2", tester);
- setBcpMetrics(100, 0.1, 0.1, context.instanceId(), eu1, "cluster2", tester);
-
- assertEquals(0.0, updater.maintain(), 0.0000001);
-
- assertNoBcpGroupInfo(context.instanceId(), ap1, "cluster1", tester, "No info in ap");
- assertNoBcpGroupInfo(context.instanceId(), ap2, "cluster1", tester, "No info in ap");
- assertBcpGroupInfo(300.0, 0.3, 0.3, context.instanceId(), us1, "cluster1", tester);
- assertBcpGroupInfo(300.0, 0.3, 0.3, context.instanceId(), us2, "cluster1", tester);
- assertBcpGroupInfo(100.0, 0.1, 0.1, context.instanceId(), us3, "cluster1", tester);
- assertBcpGroupInfo(300.0, 0.3, 0.3, context.instanceId(), eu1, "cluster1", tester);
-
- assertBcpGroupInfo(200.0, 0.2, 0.2, context.instanceId(), ap1, "cluster2", tester);
- assertBcpGroupInfo(100.0, 0.1, 0.1, context.instanceId(), ap2, "cluster2", tester);
- assertBcpGroupInfo(300.0, 0.3, 0.3, context.instanceId(), us1, "cluster2", tester);
- assertBcpGroupInfo(300.0, 0.3, 0.3, context.instanceId(), us2, "cluster2", tester);
- assertBcpGroupInfo(100.0, 0.1, 0.1, context.instanceId(), us3, "cluster2", tester);
- assertBcpGroupInfo((200 + 300) / 2.0, (0.2 + 0.3) / 2.0, (0.2 + 0.3) / 2.0, context.instanceId(), eu1, "cluster2", tester);
- }
-
- private void setQpsMetric(double qps, ApplicationId application, ZoneId zone, DeploymentTester tester) {
- var clusterMetrics = new ClusterMetrics("default", "container", Map.of(ClusterMetrics.QUERIES_PER_SECOND, qps));
- tester.controllerTester().serviceRegistry().configServerMock().setMetrics(new DeploymentId(application, zone), clusterMetrics);
- }
-
- private void assertTrafficFraction(double currentReadShare, double maxReadShare,
- ApplicationId application, ZoneId zone, DeploymentTester tester) {
- NodeRepositoryMock mock = (NodeRepositoryMock)tester.controller().serviceRegistry().configServer().nodeRepository();
- assertEquals(currentReadShare, mock.getTrafficFraction(application, zone).getFirst(), 0.00001, "Current read share");
- assertEquals(maxReadShare, mock.getTrafficFraction(application, zone).getSecond(), 0.00001, "Max read share");
- }
-
- private void setBcpMetrics(double queryRate, double growthRateHeadroom, double cpuCostPerQuery,
- ApplicationId applicationId, ZoneId zone, String clusterId, DeploymentTester tester) {
- var application = tester.controller().applications().deploymentInfo().computeIfAbsent(new DeploymentId(applicationId, zone),
- __ -> new Application(applicationId, List.of()));
- // ALl this is to pass Cluster.Autoscaling.Metrics - everything else is ignored
- var id = new ClusterSpec.Id(clusterId);
- var resources = new ClusterResources(10, 1, new NodeResources(10, 100, 1000, 0.1));
- var autoscaling = new Cluster.Autoscaling("ignored",
- "ignored",
- Optional.empty(),
- Clock.systemUTC().instant(),
- Load.zero(),
- Load.zero(),
- new Cluster.Autoscaling.Metrics(queryRate, growthRateHeadroom, cpuCostPerQuery));
- application.clusters().put(id, new Cluster(id,
- ClusterSpec.Type.container,
- resources,
- resources,
- IntRange.empty(),
- resources,
- autoscaling,
- Cluster.Autoscaling.empty(),
- List.of(),
- Duration.ofHours(1)));
- }
-
- private void assertBcpGroupInfo(double queryRate, double growthRateHeadroom, double cpuCostPerQuery,
- ApplicationId application, ZoneId zone, String clusterId, DeploymentTester tester) {
- NodeRepositoryMock mock = (NodeRepositoryMock)tester.controller().serviceRegistry().configServer().nodeRepository();
- var info = mock.getBcpGroupInfo(application, zone, new ClusterSpec.Id(clusterId));
- assertNotNull(info, "Bcp group info of " + application + " cluster " + clusterId + " in " + zone);
- assertEquals(queryRate, info.queryRate(), 0.00001, "Query rate");
- assertEquals(growthRateHeadroom, info.growthRateHeadroom(), 0.00001, "Growth rate headroom");
- assertEquals(cpuCostPerQuery, info.cpuCostPerQuery(), 0.00001, "Cpu cost per query");
- }
-
- private void assertNoBcpGroupInfo(ApplicationId application, ZoneId zone, String clusterId, DeploymentTester tester, String explanation) {
- NodeRepositoryMock mock = (NodeRepositoryMock) tester.controller().serviceRegistry().configServer().nodeRepository();
- var info = mock.getBcpGroupInfo(application, zone, new ClusterSpec.Id(clusterId));
- assertNull(info, "No bcp group info of " + application + " cluster " + clusterId + " in " + zone + ": " + explanation);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainerTest.java
deleted file mode 100644
index 6adabad557d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/BillingReportMaintainerTest.java
+++ /dev/null
@@ -1,199 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillStatus;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClient;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingReporterMock;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.InvoiceUpdate;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistryMock;
-import com.yahoo.vespa.hosted.controller.tenant.BillingReference;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.LocalDate;
-import java.time.ZoneOffset;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class BillingReportMaintainerTest {
- private final ControllerTester tester = new ControllerTester(SystemName.PublicCd);
- private final BillingReportMaintainer maintainer = new BillingReportMaintainer(tester.controller(), Duration.ofMinutes(10));
- private final BillingDatabaseClient billingDb = tester.controller().serviceRegistry().billingDatabase();
- private final BillingReporterMock reporter = (BillingReporterMock) tester.controller().serviceRegistry().billingReporter();
-
- @Test
- void only_billable_tenants_are_maintained() {
- var t1 = tester.createTenant("t1");
- var t2 = tester.createTenant("t2");
-
- tester.controller().serviceRegistry().billingController().setPlan(t1, PlanRegistryMock.paidPlan.id(), false, true);
- maintainer.maintain();
-
- var b1 = billingReference(t1);
- var b2 = billingReference(t2);
-
- assertFalse(b1.isEmpty());
- assertTrue(b2.isEmpty());
-
- assertEquals(tester.clock().instant(), b1.orElseThrow().updated());
- assertNotNull(b1.orElseThrow().reference());
- }
-
- @Test
- void only_non_final_bills_with_exported_id_are_maintained() {
- var t1 = tester.createTenant("t1");
-
- var bill1 = createBill(t1, "non-exported", billingDb);
- var bill2 = createBill(t1, "exported-and-modified", billingDb);
- var bill3 = createBill(t1, "exported-and-frozen", billingDb);
- var bill4 = createBill(t1, "exported-and-successful", billingDb);
- var bill5 = createBill(t1, "exported-and-void", billingDb);
- billingDb.setStatus(bill3, "foo", BillStatus.FROZEN);
- billingDb.setStatus(bill4, "foo", BillStatus.SUCCESSFUL);
- billingDb.setStatus(bill5, "foo", BillStatus.VOID);
-
- exportBills(t1, bill2, bill3);
- reporter.modifyInvoice(bill2);
- var updates = toMap(maintainer.maintainInvoices());
-
- assertTrue(billingDb.readBill(bill1).get().getExportedId().isEmpty());
-
- // Only the exported non-final bills are maintained
- assertEquals(2, updates.size());
- assertEquals(Set.of(bill2, bill3), updates.keySet());
-
- var bill2Update = updates.get(bill2);
- assertEquals(InvoiceUpdate.Type.MODIFIED, bill2Update.type());
- var exportedBill = billingDb.readBill(bill2).get();
- assertEquals("EXPORTED-" + exportedBill.id().value(), exportedBill.getExportedId().get());
- // Verify that the bill has been updated with a marker line item by the mock
- var lineItems = exportedBill.lineItems();
- assertEquals(1, lineItems.size());
- assertEquals("maintained", lineItems.get(0).id());
-
- // Verify that the frozen bill is unmodified and has not changed state.
- var bill3Update = updates.get(bill3);
- assertEquals(InvoiceUpdate.Type.UNMODIFIED, bill3Update.type());
- var frozenBill = billingDb.readBill(bill3).get();
- assertEquals(BillStatus.FROZEN, frozenBill.status());
- }
-
- @Test
- void bills_whose_invoice_has_been_deleted_in_the_external_system_are_no_longer_maintained() {
- var t1 = tester.createTenant("t1");
- var bill1 = createBill(t1, "exported-then-deleted", billingDb);
- exportBills(t1, bill1);
-
- var updates = maintainer.maintainInvoices();
- assertEquals(1, updates.size());
- assertEquals(InvoiceUpdate.Type.UNMODIFIED, updates.get(0).type());
-
- // Delete invoice from the external system
- reporter.deleteInvoice(bill1);
-
- // Maintainer should report that the invoice has been removed
- updates = maintainer.maintainInvoices();
- assertEquals(1, updates.size());
- assertEquals(InvoiceUpdate.Type.REMOVED, updates.get(0).type());
-
- // The bill should no longer be maintained
- updates = maintainer.maintainInvoices();
- assertEquals(0, updates.size());
- }
-
- @Test
- void it_is_allowed_to_re_export_bills_whose_invoice_has_been_deleted_in_the_external_system() {
- var t1 = tester.createTenant("t1");
- var bill1 = createBill(t1, "exported-then-deleted", billingDb);
-
- // Export the bill, then delete it in the external system
- exportBills(t1, bill1);
- maintainer.maintainInvoices();
- reporter.deleteInvoice(bill1);
- maintainer.maintainInvoices();
-
- // Ensure it is currently ignored by the maintainer
- var updates = maintainer.maintainInvoices();
- assertEquals(0, updates.size());
-
- // Re-export the bill and verify that it is maintained again
- exportBills(t1, bill1);
- updates = maintainer.maintainInvoices();
- assertEquals(1, updates.size());
- assertEquals(InvoiceUpdate.Type.UNMODIFIED, updates.get(0).type());
- }
-
- @Test
- void bill_state_is_updated_upon_changes_in_the_external_system() {
- var t1 = tester.createTenant("t1");
- var frozen = createBill(t1, "foo", billingDb);
- var paid = createBill(t1, "foo", billingDb);
- var voided = createBill(t1, "foo", billingDb);
- exportBills(t1, frozen, paid, voided);
-
- var updates = toMap(maintainer.maintainInvoices());
- assertEquals(3, updates.size());
- updates.forEach((id, update) -> {
- assertEquals(InvoiceUpdate.Type.UNMODIFIED, update.type());
- assertEquals(BillStatus.OPEN, billingDb.readBill(id).get().status());
- });
-
- reporter.freezeInvoice(frozen);
- reporter.payInvoice(paid);
- reporter.voidInvoice(voided);
- updates = toMap(maintainer.maintainInvoices());
-
- assertEquals(3, updates.size());
-
- assertEquals(InvoiceUpdate.Type.UNMODIFIABLE, updates.get(frozen).type());
- assertEquals(BillStatus.FROZEN, billingDb.readBill(frozen).get().status());
-
- assertEquals(InvoiceUpdate.Type.PAID, updates.get(paid).type());
- assertEquals(BillStatus.SUCCESSFUL, billingDb.readBill(paid).get().status());
-
- assertEquals(InvoiceUpdate.Type.VOIDED, updates.get(voided).type());
- assertEquals(BillStatus.VOID, billingDb.readBill(voided).get().status());
- }
-
- private static Map<Bill.Id, InvoiceUpdate> toMap(Iterable<InvoiceUpdate> updates) {
- var map = new HashMap<Bill.Id, InvoiceUpdate>();
- for (var update : updates) {
- map.put(update.billId(), update);
- }
- return map;
- }
-
- private static Bill.Id createBill(TenantName tenantName, String agent, BillingDatabaseClient billingDb) {
- var start = LocalDate.of(2020, 5, 23).atStartOfDay(ZoneOffset.UTC);
- var end = start.toLocalDate().plusDays(6).atStartOfDay(ZoneOffset.UTC);
- return billingDb.createBill(tenantName, start, end, agent);
- }
-
- private void exportBills(TenantName tenantName, Bill.Id... billIds) {
- for (var billId : billIds) {
- var bill = billingDb.readBill(billId).get();
- reporter.exportBill(bill, "FOO", cloudTenant(tenantName));
- }
- }
-
- private CloudTenant cloudTenant(TenantName tenantName) {
- return tester.controller().tenants().require(tenantName, CloudTenant.class);
- }
-
- private Optional<BillingReference> billingReference(TenantName tenantName) {
- return cloudTenant(tenantName).billingReference();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CertificatePoolMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CertificatePoolMaintainerTest.java
deleted file mode 100644
index 1765d1ff86d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CertificatePoolMaintainerTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.jdisc.test.MockMetric;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProviderMock;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateRequest.DnsNameStatus;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author andreer
- */
-public class CertificatePoolMaintainerTest {
-
- private final ControllerTester tester = new ControllerTester();
- private final CertificatePoolMaintainer maintainer = new CertificatePoolMaintainer(tester.controller(), new MockMetric(), Duration.ofHours(1));
-
- @Test
- void new_certs_are_requested_until_limit() {
- tester.flagSource().withIntFlag(PermanentFlags.CERT_POOL_SIZE.id(), 3);
- assertNumCerts(1);
- assertNumCerts(2);
- assertNumCerts(3);
- assertNumCerts(3);
- }
-
- @Test
- void cert_contains_expected_names() {
- tester.flagSource().withIntFlag(PermanentFlags.CERT_POOL_SIZE.id(), 1);
- assertNumCerts(1);
- EndpointCertificateProviderMock endpointCertificateProvider = (EndpointCertificateProviderMock) tester.controller().serviceRegistry().endpointCertificateProvider();
-
- var request = endpointCertificateProvider.listCertificates().get(0);
-
- assertEquals(
- List.of(
- new DnsNameStatus("*.f5549014.z.vespa.oath.cloud", "done"),
- new DnsNameStatus("*.f5549014.g.vespa.oath.cloud", "done"),
- new DnsNameStatus("*.f5549014.a.vespa.oath.cloud", "done")
- ), request.dnsNames());
-
- assertEquals("vespa.tls.preprovisioned.f5549014-cert", endpointCertificateProvider.certificateDetails(request.requestId()).certKeyKeyname());
- assertEquals("vespa.tls.preprovisioned.f5549014-key", endpointCertificateProvider.certificateDetails(request.requestId()).privateKeyKeyname());
- }
-
- private void assertNumCerts(int n) {
- assertEquals(0.0, maintainer.maintain(), 0.0000001);
- assertEquals(n, tester.curator().readUnassignedCertificates().size());
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessorTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessorTest.java
deleted file mode 100644
index 0f8aa2885e2..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeManagementAssessorTest.java
+++ /dev/null
@@ -1,188 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock;
-import org.junit.jupiter.api.Test;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author smorgrav
- */
-public class ChangeManagementAssessorTest {
-
- private final ChangeManagementAssessor changeManagementAssessor = new ChangeManagementAssessor(new NodeRepositoryMock());
-
- @Test
- void empty_input_variations() {
- ZoneId zone = ZoneId.from("prod", "eu-trd");
- List<String> hostNames = new ArrayList<>();
- List<Node> allNodesInZone = new ArrayList<>();
-
- // Both zone and hostnames are empty
- ChangeManagementAssessor.Assessment assessment
- = changeManagementAssessor.assessmentInner(hostNames, allNodesInZone, zone);
- assertEquals(0, assessment.getClusterAssessments().size());
- }
-
- @Test
- void one_host_one_cluster_no_groups() {
- ZoneId zone = ZoneId.from("prod", "eu-trd");
- List<String> hostNames = Collections.singletonList("host1");
- List<Node> allNodesInZone = new ArrayList<>();
- allNodesInZone.add(createNode("node1", "host1", "default", 0));
- allNodesInZone.add(createNode("node2", "host1", "default", 0));
- allNodesInZone.add(createNode("node3", "host1", "default", 0));
-
- // Add an not impacted hosts
- allNodesInZone.add(createNode("node4", "host2", "default", 0));
-
- // Add tenant hosts
- allNodesInZone.add(createHost("host1", NodeType.host));
- allNodesInZone.add(createHost("host2", NodeType.host));
-
- // Make Assessment
- List<ChangeManagementAssessor.ClusterAssessment> assessments
- = changeManagementAssessor.assessmentInner(hostNames, allNodesInZone, zone).getClusterAssessments();
-
- // Assess the assessment :-o
- assertEquals(1, assessments.size());
- assertEquals(3, assessments.get(0).clusterImpact);
- assertEquals(4, assessments.get(0).clusterSize);
- assertEquals(1, assessments.get(0).groupsImpact);
- assertEquals(1, assessments.get(0).groupsTotal);
- assertEquals("content:default", assessments.get(0).cluster);
- assertEquals("mytenant:myapp:default", assessments.get(0).app);
- assertEquals("prod.eu-trd", assessments.get(0).zone);
- }
-
- @Test
- void one_of_two_groups_in_one_of_two_clusters() {
- ZoneId zone = ZoneId.from("prod", "eu-trd");
- List<String> hostNames = List.of("host1", "host2", "host5");
- List<Node> allNodesInZone = new ArrayList<>();
-
- // Two impacted nodes on host1
- allNodesInZone.add(createNode("node1", "host1", "default", 0));
- allNodesInZone.add(createNode("node2", "host1", "default", 0));
-
- // One impacted nodes on host2
- allNodesInZone.add(createNode("node3", "host2", "default", 0));
-
- // Another group on hosts not impacted
- allNodesInZone.add(createNode("node4", "host3", "default", 1));
- allNodesInZone.add(createNode("node5", "host3", "default", 1));
- allNodesInZone.add(createNode("node6", "host3", "default", 1));
-
- // Another cluster on hosts not impacted - this one also with three different groups (should all be ignored here)
- allNodesInZone.add(createNode("node4", "host4", "myman", 4));
- allNodesInZone.add(createNode("node5", "host4", "myman", 5));
- allNodesInZone.add(createNode("node6", "host4", "myman", 6));
-
- // Add tenant hosts
- allNodesInZone.add(createHost("host1", NodeType.host));
- allNodesInZone.add(createHost("host2", NodeType.host));
-
-
- // Make Assessment
- ChangeManagementAssessor.Assessment assessment
- = changeManagementAssessor.assessmentInner(hostNames, allNodesInZone, zone);
-
- // Assess the assessment :-o
- List<ChangeManagementAssessor.ClusterAssessment> clusterAssessments = assessment.getClusterAssessments();
- assertEquals(1, clusterAssessments.size()); //One cluster is impacted
- assertEquals(3, clusterAssessments.get(0).clusterImpact);
- assertEquals(6, clusterAssessments.get(0).clusterSize);
- assertEquals(1, clusterAssessments.get(0).groupsImpact);
- assertEquals(2, clusterAssessments.get(0).groupsTotal);
- assertEquals("content:default", clusterAssessments.get(0).cluster);
- assertEquals("mytenant:myapp:default", clusterAssessments.get(0).app);
- assertEquals("prod.eu-trd", clusterAssessments.get(0).zone);
- assertEquals("Impact not larger than upgrade policy", clusterAssessments.get(0).impact);
-
- List<ChangeManagementAssessor.HostAssessment> hostAssessments = assessment.getHostAssessments();
- assertEquals(2, hostAssessments.size());
- assertTrue(hostAssessments.stream().anyMatch(hostAssessment ->
- hostAssessment.hostName.equals("host1") &&
- hostAssessment.switchName.equals("switch1") &&
- hostAssessment.numberOfChildren == 2 &&
- hostAssessment.numberOfProblematicChildren == 2
- ));
- }
-
- @Test
- void two_config_nodes() {
- var zone = ZoneId.from("prod", "eu-trd");
- var hostNames = List.of("config1", "config2");
- var allNodesInZone = new ArrayList<Node>();
-
- // Add config nodes and parents
- allNodesInZone.add(createNode("config1", "confighost1", "config", 0, NodeType.config));
- allNodesInZone.add(createHost("confighost1", NodeType.confighost));
- allNodesInZone.add(createNode("config2", "confighost2", "config", 0, NodeType.config));
- allNodesInZone.add(createHost("confighost2", NodeType.confighost));
-
- var assessment = changeManagementAssessor.assessmentInner(hostNames, allNodesInZone, zone).getClusterAssessments();
- var configAssessment = assessment.get(0);
- assertEquals("Large impact. Consider reprovisioning one or more config servers", configAssessment.impact);
- assertEquals(2, configAssessment.clusterImpact);
- }
-
- @Test
- void one_of_three_proxy_nodes() {
- var zone = ZoneId.from("prod", "eu-trd");
- var hostNames = List.of("routing1");
- var allNodesInZone = new ArrayList<Node>();
-
- // Add routing nodes and parents
- allNodesInZone.add(createNode("routing1", "parentrouting1", "routing", 0, NodeType.proxy));
- allNodesInZone.add(createHost("parentrouting1", NodeType.proxyhost));
- allNodesInZone.add(createNode("routing2", "parentrouting2", "routing", 0, NodeType.proxy));
- allNodesInZone.add(createHost("parentrouting2", NodeType.proxyhost));
- allNodesInZone.add(createNode("routing3", "parentrouting3", "routing", 0, NodeType.proxy));
- allNodesInZone.add(createHost("parentrouting3", NodeType.proxyhost));
-
- var assessment = changeManagementAssessor.assessmentInner(hostNames, allNodesInZone, zone).getClusterAssessments();
- assertEquals("33% of routing nodes impacted. Consider reprovisioning if too many", assessment.get(0).impact);
- }
-
- private Node createNode(String nodename, String hostname, String clusterId, int group) {
- return createNode(nodename, hostname, clusterId, group, NodeType.tenant);
- }
-
- private Node createNode(String nodename, String hostname, String clusterId, int group, NodeType nodeType) {
- return Node.builder().hostname(nodename)
- .parentHostname(hostname)
- .state(Node.State.active)
- .owner(ApplicationId.from("mytenant", "myapp", "default"))
- .group(String.valueOf(group))
- .clusterId(clusterId)
- .clusterType(Node.ClusterType.content)
- .type(nodeType)
- .build();
- }
-
- private Node createHost(String hostname, NodeType nodeType) {
- return Node.builder()
- .hostname(hostname)
- .switchHostname("switch1")
- .state(Node.State.active)
- .owner(ApplicationId.from("mytenant", "myapp", "default"))
- .group(String.valueOf(0))
- .clusterId(nodeType.name())
- .clusterType(Node.ClusterType.content)
- .type(nodeType)
- .build();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java
deleted file mode 100644
index 620a0505db8..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ChangeRequestMaintainerTest.java
+++ /dev/null
@@ -1,113 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource.Status;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.MockChangeRequestClient;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.ZonedDateTime;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author olaa
- */
-public class ChangeRequestMaintainerTest {
-
- private final ControllerTester tester = new ControllerTester();
- private final MockChangeRequestClient changeRequestClient = tester.serviceRegistry().changeRequestClient();
- private final ChangeRequestMaintainer changeRequestMaintainer = new ChangeRequestMaintainer(tester.controller(), Duration.ofMinutes(1));
-
- @Test
- void updates_status_time_and_approval() {
- var time = ZonedDateTime.now();
- var persistedChangeRequest = persistedChangeRequest("some-id", time.minusDays(5), Status.WAITING_FOR_APPROVAL);
- tester.curator().writeChangeRequest(persistedChangeRequest);
-
- var updatedChangeRequest = newChangeRequest("some-id", ChangeRequest.Approval.APPROVED, time, Status.CANCELED);
- changeRequestClient.setUpcomingChangeRequests(List.of(updatedChangeRequest));
- changeRequestMaintainer.maintain();
-
- persistedChangeRequest = tester.curator().readChangeRequest("some-id").get();
- assertEquals(Status.CANCELED, persistedChangeRequest.getChangeRequestSource().status());
- assertEquals(ChangeRequest.Approval.APPROVED, persistedChangeRequest.getApproval());
- assertEquals(time, persistedChangeRequest.getChangeRequestSource().plannedStartTime());
- assertEquals(0, changeRequestClient.getApprovedChangeRequests().size());
- }
-
- @Test
- void deletes_old_change_requests() {
- var now = ZonedDateTime.now();
- var before = now.minus(Duration.ofDays(8));
- var newChangeRequest = persistedChangeRequest("new", now, Status.CLOSED);
- var oldChangeRequest = persistedChangeRequest("old", before, Status.CLOSED);
-
- tester.curator().writeChangeRequest(newChangeRequest);
- tester.curator().writeChangeRequest(oldChangeRequest);
-
- changeRequestMaintainer.maintain();
-
- var persistedChangeRequests = tester.curator().readChangeRequests();
- assertEquals(1, persistedChangeRequests.size());
- assertEquals(newChangeRequest, persistedChangeRequests.get(0));
- }
-
- @Test
- void approves_change_request_if_non_prod() {
- var time = ZonedDateTime.now();
- var prodChangeRequest = newChangeRequest("id1", ChangeRequest.Approval.REQUESTED, time, Status.WAITING_FOR_APPROVAL);
- var nonProdApprovalRequested = newChangeRequest("id2", "unknown-node", ChangeRequest.Approval.REQUESTED, time, Status.WAITING_FOR_APPROVAL);
- var nonProdApproved = newChangeRequest("id3", "unknown-node", ChangeRequest.Approval.APPROVED, time, Status.WAITING_FOR_APPROVAL);
-
- changeRequestClient.setUpcomingChangeRequests(List.of(
- prodChangeRequest,
- nonProdApprovalRequested,
- nonProdApproved
- ));
- changeRequestMaintainer.maintain();
-
- var persistedChangeRequests = tester.curator().readChangeRequests();
- assertEquals(1, persistedChangeRequests.size());
- assertEquals(prodChangeRequest.getId(), persistedChangeRequests.get(0).getId());
-
- assertEquals(1, changeRequestClient.getApprovedChangeRequests().size());
- assertEquals(nonProdApprovalRequested.getId(), changeRequestClient.getApprovedChangeRequests().get(0).getId());
- }
-
- private ChangeRequest newChangeRequest(String id, ChangeRequest.Approval approval, ZonedDateTime time, Status status) {
- return newChangeRequest(id, "node-1-tenant-host-prod.us-east-3", approval, time, status);
- }
-
- private ChangeRequest newChangeRequest(String id, String hostname, ChangeRequest.Approval approval, ZonedDateTime time, Status status) {
- return new ChangeRequest.Builder()
- .id(id)
- .approval(approval)
- .impact(ChangeRequest.Impact.VERY_HIGH)
- .impactedSwitches(List.of())
- .impactedHosts(List.of(hostname))
- .changeRequestSource(new ChangeRequestSource.Builder()
- .plannedStartTime(time)
- .plannedEndTime(time)
- .id("some-id")
- .url("some-url")
- .system("some-system")
- .status(status)
- .category("some-category")
- .build())
- .build();
- }
-
- private VespaChangeRequest persistedChangeRequest(String id, ZonedDateTime time, Status status) {
- return new VespaChangeRequest(
- newChangeRequest(id, ChangeRequest.Approval.REQUESTED, time, status),
- ZoneId.from("prod.us-east-3")
- );
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java
deleted file mode 100644
index 02d0a020cd2..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CloudTrialExpirerTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.test.ManualClock;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.notification.Notification;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.nio.file.StandardOpenOption;
-import java.time.Duration;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author ogronnesby
- */
-public class CloudTrialExpirerTest {
-
- private static final boolean OVERWRITE_TEST_FILES = false;
-
- private final ControllerTester tester = new ControllerTester(SystemName.PublicCd);
- private final DeploymentTester deploymentTester = new DeploymentTester(tester);
- private final CloudTrialExpirer expirer = new CloudTrialExpirer(tester.controller(), Duration.ofMinutes(5));
-
- @Test
- void expire_inactive_tenant() {
- registerTenant("trial-tenant", "trial", Duration.ofDays(14).plusMillis(1));
- assertEquals(0.0, expirer.maintain());
- assertPlan("trial-tenant", "none");
- }
-
- @Test
- void tombstone_inactive_none() {
- registerTenant("none-tenant", "none", Duration.ofDays(91).plusMillis(1));
- assertEquals(0.0, expirer.maintain());
- assertEquals(Tenant.Type.deleted, tester.controller().tenants().get(TenantName.from("none-tenant"), true).get().type());
- }
-
- @Test
- void keep_inactive_nontrial_tenants() {
- registerTenant("not-a-trial-tenant", "pay-as-you-go", Duration.ofDays(30));
- assertEquals(0.0, expirer.maintain());
- assertPlan("not-a-trial-tenant", "pay-as-you-go");
- }
-
- @Test
- void keep_active_trial_tenants() {
- registerTenant("active-trial-tenant", "trial", Duration.ofHours(14).minusMillis(1));
- assertEquals(0.0, expirer.maintain());
- assertPlan("active-trial-tenant", "trial");
- }
-
- @Test
- void keep_inactive_exempt_tenants() {
- registerTenant("exempt-trial-tenant", "trial", Duration.ofDays(40));
- ((InMemoryFlagSource) tester.controller().flagSource()).withListFlag(PermanentFlags.EXTENDED_TRIAL_TENANTS.id(), List.of("exempt-trial-tenant"), String.class);
- assertEquals(0.0, expirer.maintain());
- assertPlan("exempt-trial-tenant", "trial");
- }
-
- @Test
- void keep_inactive_trial_tenants_with_deployments() {
- registerTenant("with-deployments", "trial", Duration.ofDays(30));
- registerDeployment("with-deployments", "my-app", "default");
- assertEquals(0.0, expirer.maintain());
- assertPlan("with-deployments", "trial");
- }
-
- @Test
- void delete_tenants_with_applications_with_no_deployments() {
- registerTenant("with-apps", "trial", Duration.ofDays(184));
- tester.createApplication("with-apps", "app1", "instance1");
- assertEquals(0.0, expirer.maintain());
- assertPlan("with-apps", "none");
- assertEquals(0.0, expirer.maintain());
- assertTrue(tester.controller().tenants().get("with-apps").isEmpty());
- }
-
- @Test
- void keep_tenants_without_applications_that_are_idle() {
- registerTenant("active", "none", Duration.ofDays(182));
- assertEquals(0.0, expirer.maintain());
- assertPlan("active", "none");
- }
-
- @Test
- void queues_trial_notification_based_on_account_age() throws IOException {
- var clock = (ManualClock)tester.controller().clock();
- var mailer = (MockMailer) tester.serviceRegistry().mailer();
- var tenant = TenantName.from("trial-tenant");
- ((InMemoryFlagSource) tester.controller().flagSource())
- .withBooleanFlag(Flags.CLOUD_TRIAL_NOTIFICATIONS.id(), true);
- registerTenant(tenant.value(), "trial", Duration.ZERO);
- assertEquals(0.0, expirer.maintain());
- var expectedConsoleNotification =
- "Welcome to Vespa Cloud trial! [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)";
- var notification = lastAccountLevelNotification(tenant);
- assertEquals(expectedConsoleNotification, notification.title());
- assertLastEmail(mailer, notification);
-
- expectedConsoleNotification =
- "You're halfway through the **14 day** trial period. [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)";
- clock.advance(Duration.ofDays(7));
- assertEquals(0.0, expirer.maintain());
- notification = lastAccountLevelNotification(tenant);
- assertEquals(expectedConsoleNotification, notification.title());
- assertLastEmail(mailer, notification);
-
- expectedConsoleNotification = "Your Vespa Cloud trial expires **tomorrow**. [Manage plan](https://console.tld/tenant/trial-tenant/account/billing)";
- clock.advance(Duration.ofDays(6));
- assertEquals(0.0, expirer.maintain());
- notification = lastAccountLevelNotification(tenant);
- assertEquals(expectedConsoleNotification, notification.title());
- assertLastEmail(mailer, notification);
-
- expectedConsoleNotification = "Your Vespa Cloud trial has expired. [Upgrade plan](https://console.tld/tenant/trial-tenant/account/billing)";
- clock.advance(Duration.ofDays(2));
- assertEquals(0.0, expirer.maintain());
- notification = lastAccountLevelNotification(tenant);
- assertEquals(expectedConsoleNotification, notification.title());
- assertLastEmail(mailer, notification);
- }
-
- private void assertLastEmail(MockMailer mailer, Notification notification) throws IOException {
- var mails = mailer.inbox("dev-trial-tenant");
- assertFalse(mails.isEmpty());
- var content = mails.get(mails.size() - 1).htmlMessage().orElseThrow();
- var templateName = notification.mailContent().orElseThrow().values().get("mailMessageTemplate");
- var path = Paths.get("src/test/resources/mail/%s.html".formatted(templateName));
- if (OVERWRITE_TEST_FILES) {
- Files.write(path, content.getBytes(),
- StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
- } else {
- var expectedContent = Files.readString(path);
- assertEquals(expectedContent, content);
- }
- }
-
- private void registerTenant(String tenantName, String plan, Duration timeSinceLastLogin) {
- var name = TenantName.from(tenantName);
- tester.createTenant(tenantName, Tenant.Type.cloud);
- tester.serviceRegistry().billingController().setPlan(name, PlanId.from(plan), false, false);
- tester.controller().tenants().updateLastLogin(name, List.of(LastLoginInfo.UserLevel.user), tester.controller().clock().instant().minus(timeSinceLastLogin));
- }
-
- private void registerDeployment(String tenantName, String appName, String instanceName) {
- var app = tester.createApplication(tenantName, appName, instanceName);
- var ctx = deploymentTester.newDeploymentContext(tenantName, appName, instanceName);
- var pkg = new ApplicationPackageBuilder()
- .instances("default")
- .region("aws-us-east-1c")
- .trustDefaultCertificate()
- .build();
- ctx.submit(pkg).deploy();
- }
-
- private void assertPlan(String tenant, String planId) {
- assertEquals(planId, tester.serviceRegistry().billingController().getPlan(TenantName.from(tenant)).value());
- }
-
- private Notification lastAccountLevelNotification(TenantName tenant) {
- return tester.controller().notificationsDb()
- .listNotifications(NotificationSource.from(tenant), false).stream()
- .filter(n -> n.type() == Notification.Type.account)
- .findFirst().orElseThrow();
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainerTest.java
deleted file mode 100644
index eb7458af0f7..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ContactInformationMaintainerTest.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.time.Duration;
-import java.util.Collections;
-import java.util.List;
-import java.util.Optional;
-import java.util.function.Supplier;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-
-/**
- * @author mpolden
- */
-public class ContactInformationMaintainerTest {
-
- private ControllerTester tester;
- private ContactInformationMaintainer maintainer;
-
- @BeforeEach
- public void before() {
- tester = new ControllerTester();
- maintainer = new ContactInformationMaintainer(tester.controller(), Duration.ofDays(1), 1.0);
- }
-
- @Test
- void updates_contact_information() {
- PropertyId propertyId1 = new PropertyId("1");
- PropertyId propertyId2 = new PropertyId("2");
- TenantName name1 = tester.createTenant("tenant1", "domain1", 1L);
- TenantName name2 = tester.createTenant("zenant1", "domain2", 2L);
- Supplier<AthenzTenant> tenant1 = () -> (AthenzTenant) tester.controller().tenants().require(name1);
- Supplier<AthenzTenant> tenant2 = () -> (AthenzTenant) tester.controller().tenants().require(name2);
- assertFalse(tenant1.get().contact().isPresent(), "No contact information initially");
- assertFalse(tenant2.get().contact().isPresent(), "No contact information initially");
-
- Contact contact = testContact();
- tester.serviceRegistry().contactRetriever().addContact(propertyId1, () -> {
- throw new RuntimeException("ERROR");
- });
- tester.serviceRegistry().contactRetriever().addContact(propertyId2, () -> contact);
- maintainer.maintain();
-
- assertEquals(Optional.empty(), tenant1.get().contact(), "No contact information added due to error");
- assertEquals(Optional.of(contact), tenant2.get().contact(), "Contact information added");
- }
-
- private static Contact testContact() {
- URI contactUrl = URI.create("http://contact1.test");
- URI issueTrackerUrl = URI.create("http://issue-tracker1.test");
- URI propertyUrl = URI.create("http://property1.test");
- List<List<String>> persons = List.of(Collections.singletonList("alice"),
- Collections.singletonList("bob"));
- String queue = "queue";
- Optional<String> component = Optional.empty();
- return new Contact(contactUrl, propertyUrl, issueTrackerUrl, persons, queue, component);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainerTest.java
deleted file mode 100644
index a0312d2b52d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintainerTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.integration.MetricsMock;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.EnumSet;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class ControllerMaintainerTest {
-
- private ControllerTester tester;
-
- @BeforeEach
- public void before() {
- tester = new ControllerTester();
- }
-
- @Test
- void only_runs_in_permitted_systems() {
- AtomicInteger executions = new AtomicInteger();
- new TestControllerMaintainer(tester.controller(), SystemName.cd, executions).run();
- new TestControllerMaintainer(tester.controller(), SystemName.main, executions).run();
- assertEquals(1, executions.get());
- }
-
- @Test
- void records_metric() {
- TestControllerMaintainer maintainer = new TestControllerMaintainer(tester.controller(), SystemName.main, new AtomicInteger());
- maintainer.run();
- assertEquals(0.0, successFactorDeviationMetric(), 0.0000001);
- maintainer.success = false;
- maintainer.run();
- maintainer.run();
- assertEquals(1.0, successFactorDeviationMetric(), 0.0000001);
- maintainer.success = true;
- maintainer.run();
- assertEquals(0.0, successFactorDeviationMetric(), 0.0000001);
- }
-
- private long consecutiveFailuresMetric() {
- MetricsMock metrics = (MetricsMock) tester.controller().metric();
- return metrics.getMetric((context) -> "TestControllerMaintainer".equals(context.get("job")),
- "maintenance.consecutiveFailures").get().longValue();
- }
-
- private long successFactorDeviationMetric() {
- MetricsMock metrics = (MetricsMock) tester.controller().metric();
- return metrics.getMetric((context) -> "TestControllerMaintainer".equals(context.get("job")),
- "maintenance.successFactorDeviation").get().longValue();
- }
-
- private static class TestControllerMaintainer extends ControllerMaintainer {
-
- private final AtomicInteger executions;
- private boolean success = true;
-
- public TestControllerMaintainer(Controller controller, SystemName system, AtomicInteger executions) {
- super(controller, Duration.ofDays(1), null, EnumSet.of(system));
- this.executions = executions;
- }
-
- @Override
- protected double maintain() {
- executions.incrementAndGet();
- return success ? 0.0 : 1.0;
- }
-
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainerTest.java
deleted file mode 100644
index 8d2cc5d9b55..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/CostReportMaintainerTest.java
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.CostReportConsumerMock;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceAllocation;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author ldalves
- */
-public class CostReportMaintainerTest {
-
- private final ControllerTester tester = new ControllerTester();
-
- @Test
- void maintain() {
- tester.clock().setInstant(Instant.EPOCH);
- tester.zoneRegistry().setZones(
- ZoneApiMock.newBuilder().withId("prod.us-east-3").withCloud("yahoo").build(),
- ZoneApiMock.newBuilder().withId("prod.us-west-1").withCloud("yahoo").build(),
- ZoneApiMock.newBuilder().withId("prod.us-central-1").withCloud("yahoo").build(),
- ZoneApiMock.newBuilder().withId("prod.eu-west-1").withCloud("yahoo").build());
- addNodes();
-
- CostReportConsumerMock costReportConsumer = new CostReportConsumerMock(
- (csv) -> assertEquals(
- "Date,Property,Reserved Cpu Cores,Reserved Memory GB,Reserved Disk Space GB,Usage Fraction\n" +
- "1970-01-01,Property1,96.0,96.0,2000.0,0.3055555555555555\n" +
- "1970-01-01,Property3,128.0,96.0,2000.0,0.3333333333333333\n" +
- "1970-01-01,Property2,160.0,96.0,2000.0,0.3611111111111111",
- csv),
- Map.of(new Property("Property3"), new ResourceAllocation(256, 192, 4000, NodeResources.Architecture.getDefault()))
- );
-
-
- tester.createTenant("tenant1", "app1", 1L);
- tester.createTenant("tenant2", "app2", 2L);
- CostReportMaintainer maintainer = new CostReportMaintainer(
- tester.controller(),
- Duration.ofDays(1),
- costReportConsumer
- );
- maintainer.maintain();
- }
-
- private void addNodes() {
- for (var zone : tester.zoneRegistry().zones().all().zones()) {
- tester.configServer().nodeRepository().addFixedNodes(zone.getId());
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java
deleted file mode 100644
index 4805e1c3853..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentExpirerTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertSame;
-
-/**
- * @author bratseth
- */
-public class DeploymentExpirerTest {
-
- private final DeploymentTester tester = new DeploymentTester();
-
- @Test
- void testDeploymentExpiry() {
- ZoneId devZone = ZoneId.from(Environment.dev, RegionName.from("us-east-1"));
- tester.controllerTester().zoneRegistry().setDeploymentTimeToLive(devZone, Duration.ofDays(14));
- DeploymentExpirer expirer = new DeploymentExpirer(tester.controller(), Duration.ofDays(1));
- var devApp = tester.newDeploymentContext("tenant1", "app1", "default");
- var prodApp = tester.newDeploymentContext("tenant2", "app2", "default");
-
- ApplicationPackage appPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
-
- // Deploy dev
- devApp.runJob(DeploymentContext.devUsEast1, appPackage);
-
- // Deploy prod
- prodApp.submit(appPackage).deploy();
- assertEquals(1, permanentDeployments(devApp.instance()));
- assertEquals(1, permanentDeployments(prodApp.instance()));
-
- // Not expired at first
- expirer.maintain();
- assertEquals(1, permanentDeployments(devApp.instance()));
- assertEquals(1, permanentDeployments(prodApp.instance()));
-
- // Deploy dev unsuccessfully a few days before expiry
- tester.clock().advance(Duration.ofDays(12));
- tester.configServer().throwOnNextPrepare(new RuntimeException(getClass().getSimpleName()));
- tester.jobs().deploy(devApp.instanceId(), DeploymentContext.devUsEast1, Optional.empty(), appPackage);
- Run lastRun = tester.jobs().last(devApp.instanceId(), DeploymentContext.devUsEast1).get();
- assertSame(RunStatus.error, lastRun.status());
- Deployment deployment = tester.applications().requireInstance(devApp.instanceId())
- .deployments().get(devZone);
- assertEquals(Duration.ofDays(12),
- Duration.between(deployment.at(), lastRun.end().get()),
- "Time of last run is after time of deployment");
-
- // Dev application does not expire based on time of successful deployment
- tester.clock().advance(Duration.ofDays(2));
- expirer.maintain();
- assertEquals(1, permanentDeployments(devApp.instance()));
- assertEquals(1, permanentDeployments(prodApp.instance()));
-
- // Dev application expires when enough time has passed since most recent attempt
- // Redeployments done by DeploymentUpgrader do not affect this
- tester.clock().advance(Duration.ofDays(12).plus(Duration.ofSeconds(1)));
- tester.jobs().start(devApp.instanceId(), DeploymentContext.devUsEast1, lastRun.versions(), true, Run.Reason.because("upgrade"));
- expirer.maintain();
- assertEquals(0, permanentDeployments(devApp.instance()));
- assertEquals(1, permanentDeployments(prodApp.instance()));
- }
-
- private long permanentDeployments(Instance instance) {
- return tester.controller().applications().requireInstance(instance.id()).deployments().values().stream()
- .filter(deployment -> !deployment.zone().environment().isTest())
- .count();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentInfoMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentInfoMaintainerTest.java
deleted file mode 100644
index 60dc1ea7fd5..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentInfoMaintainerTest.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Application;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author bratseth
- */
-public class DeploymentInfoMaintainerTest {
-
- @Test
- void testDeploymentInfoMaintainer() {
- ApplicationId app1 = ApplicationId.from("t1", "a1", "default");
- ApplicationId app2 = ApplicationId.from("t2", "a1", "default");
- ZoneId z1 = ZoneId.from("prod.aws-us-east-1c");
- ZoneId z2 = ZoneId.from("prod.aws-eu-west-1a");
-
- DeploymentTester tester = new DeploymentTester(new ControllerTester(SystemName.Public));
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder().region(z1.region()).region(z2.region()).trustDefaultCertificate().build();
- List.of(app1, app2).forEach(app -> tester.newDeploymentContext(app).submit(applicationPackage).deploy());
-
- var maintainer = new DeploymentInfoMaintainer(tester.controller(), Duration.ofMinutes(5), 0.95);
- var nodeRepo = tester.configServer().nodeRepository().allowPatching(true);
- nodeRepo.putApplication(z1, new Application(app1, List.of()));
- nodeRepo.putApplication(z1, new Application(app2, List.of()));
- assertEquals(0, tester.controller().applications().deploymentInfo().size());
- maintainer.maintain();
- assertEquals(4, tester.controller().applications().deploymentInfo().size());
- assertEquals(Set.of(new DeploymentId(app1, z1),
- new DeploymentId(app1, z2),
- new DeploymentId(app2, z1),
- new DeploymentId(app2, z2)),
- tester.controller().applications().deploymentInfo().keySet());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java
deleted file mode 100644
index 11b1140094b..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentIssueReporterTest.java
+++ /dev/null
@@ -1,234 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.LoggingDeploymentIssues;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.HashMap;
-import java.util.Map;
-
-import static com.yahoo.config.application.api.DeploymentSpec.UpgradePolicy.canary;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.stagingTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.systemTest;
-import static com.yahoo.vespa.hosted.controller.maintenance.DeploymentIssueReporter.maxFailureAge;
-import static com.yahoo.vespa.hosted.controller.maintenance.DeploymentIssueReporter.maxInactivity;
-import static com.yahoo.vespa.hosted.controller.maintenance.DeploymentIssueReporter.upgradeGracePeriod;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author jonmv
- */
-public class DeploymentIssueReporterTest {
-
- private final static ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
- private final static ApplicationPackage canaryPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .upgradePolicy("canary")
- .build();
-
- private DeploymentTester tester;
- private DeploymentIssueReporter reporter;
- private MockDeploymentIssues issues;
-
- @BeforeEach
- public void setup() {
- tester = new DeploymentTester();
- issues = new MockDeploymentIssues();
- reporter = new DeploymentIssueReporter(tester.controller(), issues, Duration.ofDays(1));
- }
-
- @Test
- void nonProductionAppGetsNoIssues() {
- tester.controllerTester().upgradeSystem(Version.fromString("6.2"));
- var app = tester.newDeploymentContext("application", "tenant", "default");
- Contact contact = tester.controllerTester().serviceRegistry().contactRetrieverMock().contact();
- tester.controller().tenants().lockOrThrow(app.instanceId().tenant(), LockedTenant.Athenz.class, tenant ->
- tester.controller().tenants().store(tenant.with(contact)));
-
- // app submits a package with no production deployments, and shall not receive issues.
- app.submit(new ApplicationPackageBuilder().systemTest().stagingTest().build()).runJob(systemTest).failDeployment(stagingTest);
-
- // Advance to where deployment issues should be detected.
- tester.clock().advance(maxFailureAge.plus(Duration.ofDays(1)));
- assertFalse(issues.isOpenFor(app.application().id()), "No issues are produced for app.");
- }
-
- @Test
- void testDeploymentFailureReporting() {
- tester.controllerTester().upgradeSystem(Version.fromString("6.2"));
-
- // Create and deploy one application for each of three tenants.
- var app1 = tester.newDeploymentContext("application1", "tenant1", "default");
- var app2 = tester.newDeploymentContext("application2", "tenant2", "default");
- var app3 = tester.newDeploymentContext("application3", "tenant3", "default");
-
- Contact contact = tester.controllerTester().serviceRegistry().contactRetrieverMock().contact();
- tester.controller().tenants().lockOrThrow(app1.instanceId().tenant(), LockedTenant.Athenz.class, tenant ->
- tester.controller().tenants().store(tenant.with(contact)));
- tester.controller().tenants().lockOrThrow(app2.instanceId().tenant(), LockedTenant.Athenz.class, tenant ->
- tester.controller().tenants().store(tenant.with(contact)));
- tester.controller().tenants().lockOrThrow(app3.instanceId().tenant(), LockedTenant.Athenz.class, tenant ->
- tester.controller().tenants().store(tenant.with(contact)));
-
-
- // NOTE: All maintenance should be idempotent within a small enough time interval, so maintain is called twice in succession throughout.
-
- // app 1 fails staging tests.
- app1.submit(applicationPackage).runJob(systemTest).timeOutConvergence(stagingTest);
-
- // app2 is successful, but will fail later.
- app2.submit(applicationPackage).deploy();
-
- // app 3 fails a production job.
- app3.submit(applicationPackage).runJob(systemTest).runJob(stagingTest).failDeployment(productionUsWest1);
-
- reporter.maintain();
- reporter.maintain();
- assertEquals(0, issues.size(), "No deployments are detected as failing for a long time initially.");
-
-
- // Advance to where deployment issues should be detected.
- tester.clock().advance(maxFailureAge.plus(Duration.ofDays(1)));
-
- reporter.maintain();
- reporter.maintain();
- assertTrue(issues.isOpenFor(app1.application().id()), "One issue is produced for app1.");
- assertFalse(issues.isOpenFor(app2.application().id()), "No issues are produced for app2.");
- assertTrue(issues.isOpenFor(app3.application().id()), "One issue is produced for app3.");
-
-
- // app3 closes their issue prematurely; see that it is refiled.
- issues.closeFor(app3.application().id());
- assertFalse(issues.isOpenFor(app3.application().id()), "No issue is open for app3.");
-
- reporter.maintain();
- reporter.maintain();
- assertTrue(issues.isOpenFor(app3.application().id()), "Issue is re-filed for app3.");
-
- // Some time passes; tenant1 leaves her issue unattended, while tenant3 starts work and updates the issue.
- tester.clock().advance(maxInactivity.plus(maxFailureAge));
- issues.touchFor(app3.application().id());
-
- reporter.maintain();
- reporter.maintain();
- assertEquals(1, issues.escalationLevelFor(app1.application().id()), "The issue for app1 is escalated once.");
-
-
- // app3 fixes their problems, but the ticket for app3 is left open; see the resolved ticket is not escalated when another escalation period has passed.
- app3.runJob(productionUsWest1);
- tester.clock().advance(maxInactivity.plus(Duration.ofDays(1)));
-
- reporter.maintain();
- reporter.maintain();
- assertFalse(issues.platformIssue(), "We no longer have a platform issue.");
- assertEquals(2, issues.escalationLevelFor(app1.application().id()), "The issue for app1 is escalated once more.");
- assertEquals(0, issues.escalationLevelFor(app3.application().id()), "The issue for app3 is not escalated.");
-
-
- // app3 now has a new failure past max failure age; see that a new issue is filed.
- app3.submit(applicationPackage).failDeployment(systemTest);
- tester.clock().advance(maxInactivity.plus(maxFailureAge));
-
- reporter.maintain();
- reporter.maintain();
- assertTrue(issues.isOpenFor(app3.application().id()), "A new issue is filed for app3.");
-
-
- // app2 is changed to be a canary
- app2.submit(canaryPackage).deploy();
- assertEquals(canary, app2.application().deploymentSpec().requireInstance("default").upgradePolicy());
- assertEquals(Change.empty(), app2.instance().change());
-
- // Bump system version to upgrade canary app2.
- Version version = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version);
- tester.upgrader().maintain();
- assertEquals(version, tester.controller().readSystemVersion());
-
- app2.timeOutUpgrade(systemTest);
- tester.controllerTester().upgradeSystem(version);
- assertEquals(VespaVersion.Confidence.broken, tester.controller().readVersionStatus().systemVersion().get().confidence());
-
- assertFalse(issues.platformIssue(), "We have no platform issues initially.");
- reporter.maintain();
- reporter.maintain();
- assertFalse(issues.platformIssue(), "We have no platform issue before the grace period is out for the failing canary.");
- tester.clock().advance(upgradeGracePeriod.plus(upgradeGracePeriod));
- reporter.maintain();
- reporter.maintain();
- assertTrue(issues.platformIssue(), "We get a platform issue when confidence is broken");
- assertFalse(issues.isOpenFor(app2.application().id()), "No deployment issue is filed for app2, which has a version upgrade failure.");
-
- app2.runJob(systemTest);
- tester.controllerTester().upgradeSystem(version);
- assertEquals(VespaVersion.Confidence.low, tester.controller().readVersionStatus().systemVersion().get().confidence());
- }
-
-
- class MockDeploymentIssues extends LoggingDeploymentIssues {
-
- private final Map<TenantAndApplicationId, IssueId> applicationIssues = new HashMap<>();
- private final Map<IssueId, Integer> issueLevels = new HashMap<>();
-
- MockDeploymentIssues() {
- super(tester.clock());
- }
-
- @Override
- protected void escalateIssue(IssueId issueId) {
- super.escalateIssue(issueId);
- issueLevels.merge(issueId, 1, Integer::sum);
- }
-
- @Override
- protected IssueId fileIssue(ApplicationId applicationId) {
- IssueId issueId = super.fileIssue(applicationId);
- applicationIssues.put(TenantAndApplicationId.from(applicationId), issueId);
- return issueId;
- }
-
- void closeFor(TenantAndApplicationId id) {
- issueUpdates.remove(applicationIssues.remove(id));
- }
-
- void touchFor(TenantAndApplicationId id) {
- issueUpdates.put(applicationIssues.get(id), tester.clock().instant());
- }
-
- boolean isOpenFor(TenantAndApplicationId id) {
- return applicationIssues.containsKey(id);
- }
-
- int escalationLevelFor(TenantAndApplicationId id) {
- return issueLevels.getOrDefault(applicationIssues.get(id), 0);
- }
-
- int size() {
- return issueUpdates.size();
- }
-
- boolean platformIssue() {
- return platformIssue.get();
- }
-
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java
deleted file mode 100644
index 8a441547da6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentMetricsMaintainerTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Supplier;
-
-import static java.time.temporal.ChronoUnit.MILLIS;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-
-/**
- * @author smorgrav
- * @author mpolden
- */
-public class DeploymentMetricsMaintainerTest {
-
- private final DeploymentTester tester = new DeploymentTester();
-
- @Test
- void updates_metrics() {
- Version version1 = Version.fromString("7.1");
- tester.controllerTester().upgradeSystem(version1);
- var application = tester.newDeploymentContext();
- application.runJob(DeploymentContext.devUsEast1, new ApplicationPackage(new byte[0]), version1);
-
- DeploymentMetricsMaintainer maintainer = maintainer(tester.controller());
- Supplier<Application> app = application::application;
- Supplier<Deployment> deployment = () -> application.deployment(ZoneId.from("dev", "us-east-1"));
-
- // No metrics gathered yet
- assertEquals(0, app.get().metrics().queryServiceQuality(), 0);
- assertEquals(0, deployment.get().metrics().documentCount(), 0);
- assertFalse(deployment.get().metrics().instant().isPresent(), "No timestamp set");
- assertFalse(deployment.get().activity().lastQueried().isPresent(), "Never received any queries");
- assertFalse(deployment.get().activity().lastWritten().isPresent(), "Never received any writes");
-
- // Metrics are gathered and saved to application
- Version version2 = Version.fromString("7.5.5");
- tester.controllerTester().upgradeSystem(version2);
- application.runJob(DeploymentContext.devUsEast1, new ApplicationPackage(new byte[0]), version2);
- var metrics0 = Map.of(ClusterMetrics.QUERIES_PER_SECOND, 1D,
- ClusterMetrics.FEED_PER_SECOND, 2D,
- ClusterMetrics.DOCUMENT_COUNT, 3D,
- ClusterMetrics.QUERY_LATENCY, 4D,
- ClusterMetrics.FEED_LATENCY, 5D);
- setMetrics(application.application().id().defaultInstance(), metrics0);
- maintainer.maintain();
- Instant t1 = tester.clock().instant().truncatedTo(MILLIS);
- assertEquals(0.0, app.get().metrics().queryServiceQuality(), Double.MIN_VALUE);
- assertEquals(0.0, app.get().metrics().writeServiceQuality(), Double.MIN_VALUE);
- assertEquals(1, deployment.get().metrics().queriesPerSecond(), Double.MIN_VALUE);
- assertEquals(2, deployment.get().metrics().writesPerSecond(), Double.MIN_VALUE);
- assertEquals(3, deployment.get().metrics().documentCount(), Double.MIN_VALUE);
- assertEquals(4, deployment.get().metrics().queryLatencyMillis(), Double.MIN_VALUE);
- assertEquals(5, deployment.get().metrics().writeLatencyMillis(), Double.MIN_VALUE);
- assertEquals(t1, deployment.get().metrics().instant().get());
- assertEquals(t1, deployment.get().activity().lastQueried().get());
- assertEquals(t1, deployment.get().activity().lastWritten().get());
-
- // Time passes. Activity is updated as app is still receiving traffic
- tester.clock().advance(Duration.ofHours(1));
- Instant t2 = tester.clock().instant().truncatedTo(MILLIS);
- maintainer.maintain();
- assertEquals(t2, deployment.get().metrics().instant().get());
- assertEquals(t2, deployment.get().activity().lastQueried().get());
- assertEquals(t2, deployment.get().activity().lastWritten().get());
- assertEquals(1, deployment.get().activity().lastQueriesPerSecond().getAsDouble(), Double.MIN_VALUE);
- assertEquals(2, deployment.get().activity().lastWritesPerSecond().getAsDouble(), Double.MIN_VALUE);
-
- // Query traffic disappears. Query activity stops updating
- tester.clock().advance(Duration.ofHours(1));
- Instant t3 = tester.clock().instant().truncatedTo(MILLIS);
- var metrics1 = new HashMap<>(metrics0);
- metrics1.put(ClusterMetrics.QUERIES_PER_SECOND, 0D);
- metrics1.put(ClusterMetrics.FEED_PER_SECOND, 5D);
- setMetrics(application.application().id().defaultInstance(), metrics1);
- maintainer.maintain();
- assertEquals(t2, deployment.get().activity().lastQueried().get());
- assertEquals(t3, deployment.get().activity().lastWritten().get());
- assertEquals(1, deployment.get().activity().lastQueriesPerSecond().getAsDouble(), Double.MIN_VALUE);
- assertEquals(5, deployment.get().activity().lastWritesPerSecond().getAsDouble(), Double.MIN_VALUE);
-
- // Feed traffic disappears. Feed activity stops updating
- tester.clock().advance(Duration.ofHours(1));
- var metrics2 = new HashMap<>(metrics1);
- metrics2.put(ClusterMetrics.FEED_PER_SECOND, 0D);
- setMetrics(application.application().id().defaultInstance(), metrics2);
- maintainer.maintain();
- assertEquals(t2, deployment.get().activity().lastQueried().get());
- assertEquals(t3, deployment.get().activity().lastWritten().get());
- assertEquals(1, deployment.get().activity().lastQueriesPerSecond().getAsDouble(), Double.MIN_VALUE);
- assertEquals(5, deployment.get().activity().lastWritesPerSecond().getAsDouble(), Double.MIN_VALUE);
- }
-
- @Test
- void cluster_metric_aggregation_test() {
- List<ClusterMetrics> clusterMetrics = List.of(
- new ClusterMetrics("niceCluster", "container", Map.of("queriesPerSecond", 23.0, "queryLatency", 1337.0)),
- new ClusterMetrics("alsoNiceCluster", "container", Map.of("queriesPerSecond", 11.0, "queryLatency", 12.0)));
-
- DeploymentMetrics deploymentMetrics = DeploymentMetricsMaintainer.updateDeploymentMetrics(DeploymentMetrics.none, clusterMetrics);
-
- assertEquals(23.0 + 11.0, deploymentMetrics.queriesPerSecond(), 0.001);
- assertEquals(908.323, deploymentMetrics.queryLatencyMillis(), 0.001);
- assertEquals(0, deploymentMetrics.documentCount(), 0.001);
- assertEquals(0.0, deploymentMetrics.writeLatencyMillis(), 0.001);
- assertEquals(0.0, deploymentMetrics.writesPerSecond(), 0.001);
- }
-
- private void setMetrics(ApplicationId application, Map<String, Double> metrics) {
- var clusterMetrics = new ClusterMetrics("default", "container", metrics);
- tester.controllerTester().serviceRegistry().configServerMock().setMetrics(new DeploymentId(application, ZoneId.from("dev", "us-east-1")), clusterMetrics);
- }
-
- private static DeploymentMetricsMaintainer maintainer(Controller controller) {
- return new DeploymentMetricsMaintainer(controller, Duration.ofDays(1));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgraderTest.java
deleted file mode 100644
index bc3a5808989..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/DeploymentUpgraderTest.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.devUsEast1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsWest1;
-import static com.yahoo.vespa.hosted.controller.maintenance.DeploymentUpgrader.mostLikelyWeeHour;
-import static java.time.temporal.ChronoUnit.MILLIS;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author jonmv
- */
-public class DeploymentUpgraderTest {
-
- private final DeploymentTester tester = new DeploymentTester();
-
- @Test
- void testDeploymentUpgrading() {
- ZoneId devZone = ZoneId.from(Environment.dev, RegionName.from("us-east-1"));
- DeploymentUpgrader upgrader = new DeploymentUpgrader(tester.controller(), Duration.ofDays(1));
- var devApp = tester.newDeploymentContext("tenant1", "app1", "default");
- var prodApp = tester.newDeploymentContext("tenant2", "app2", "default");
-
- ApplicationPackage appPackage = new ApplicationPackageBuilder().region("us-west-1").build();
- Version systemVersion = tester.controller().readSystemVersion();
- Instant start = tester.clock().instant().truncatedTo(MILLIS);
-
- devApp.runJob(devUsEast1, appPackage);
- prodApp.submit(appPackage).deploy();
- assertEquals(systemVersion, tester.jobs().last(devApp.instanceId(), devUsEast1).get().versions().targetPlatform());
- assertEquals(systemVersion, tester.jobs().last(prodApp.instanceId(), productionUsWest1).get().versions().targetPlatform());
-
- // Not upgraded initially
- upgrader.maintain();
- assertEquals(start, tester.jobs().last(devApp.instanceId(), devUsEast1).get().start());
- assertEquals(start, tester.jobs().last(prodApp.instanceId(), productionUsWest1).get().start());
-
- // Not upgraded immediately after system upgrades
- tester.controllerTester().upgradeSystem(new Version(7, 8, 9));
- upgrader.maintain();
- assertEquals(start, tester.jobs().last(devApp.instanceId(), devUsEast1).get().start());
- assertEquals(start, tester.jobs().last(prodApp.instanceId(), productionUsWest1).get().start());
-
- // 11 hours pass, but not upgraded since it's not likely in the middle of the night
- tester.clock().advance(Duration.ofHours(11));
- upgrader.maintain();
- assertEquals(start, tester.jobs().last(devApp.instanceId(), devUsEast1).get().start());
- assertEquals(start, tester.jobs().last(prodApp.instanceId(), productionUsWest1).get().start());
-
- // 14 hours pass, and the dev deployment, only, is upgraded
- tester.clock().advance(Duration.ofHours(3));
- upgrader.maintain();
- assertEquals(tester.clock().instant().truncatedTo(MILLIS), tester.jobs().last(devApp.instanceId(), devUsEast1).get().start());
- assertTrue(tester.jobs().last(devApp.instanceId(), devUsEast1).get().isRedeployment());
- assertEquals(start, tester.jobs().last(prodApp.instanceId(), productionUsWest1).get().start());
- devApp.runJob(devUsEast1);
-
- // After the upgrade, the dev app is mostly (re)deployed to at night, but this doesn't affect what is likely the night.
- tester.controllerTester().upgradeSystem(new Version(7, 9, 11));
- tester.clock().advance(Duration.ofHours(48));
- upgrader.maintain();
- assertEquals(tester.clock().instant().truncatedTo(MILLIS), tester.jobs().last(devApp.instanceId(), devUsEast1).get().start());
- }
-
- @Test
- void testNight() {
- assertEquals(16, mostLikelyWeeHour(new int[]{0, 1, 2, 3, 4, 5, 6}));
- assertEquals(14, mostLikelyWeeHour(new int[]{22, 23, 0, 1, 2, 3, 4}));
- assertEquals(18, mostLikelyWeeHour(new int[]{6, 5, 4, 3, 2, 1, 0}));
- assertEquals(20, mostLikelyWeeHour(new int[]{0, 12, 0, 12, 0, 12, 0, 12, 0, 12, 0, 11}));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainerTest.java
deleted file mode 100644
index 25a7044d6ea..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainerTest.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.MockEnclaveAccessService;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author jonmv
- */
-class EnclaveAccessMaintainerTest {
-
- @Test
- void test() {
- ControllerTester tester = new ControllerTester();
- MockEnclaveAccessService amis = tester.serviceRegistry().enclaveAccessService();
- EnclaveAccessMaintainer sharer = new EnclaveAccessMaintainer(tester.controller(), Duration.ofHours(1));
- CloudAccountVerifier accountVerifier = new CloudAccountVerifier(tester.controller(), Duration.ofHours(1));
- assertEquals(Set.of(), amis.currentAccounts());
-
- assertEquals(1, sharer.maintain());
- assertEquals(Set.of(), amis.currentAccounts());
-
- tester.createTenant("tanten");
- accountVerifier.maintain();
- assertEquals(1, sharer.maintain());
- assertEquals(Set.of(), amis.currentAccounts());
-
- tester.flagSource().withListFlag(PermanentFlags.CLOUD_ACCOUNTS.id(), List.of("123123123123", "321321321321"), String.class);
- accountVerifier.maintain();
- assertEquals(1, sharer.maintain());
- assertEquals(Set.of(CloudAccount.from("aws:123123123123"), CloudAccount.from("aws:321321321321")), amis.currentAccounts());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainerTest.java
deleted file mode 100644
index fe9e9b28655..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EndpointCertificateMaintainerTest.java
+++ /dev/null
@@ -1,285 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.jdisc.test.MockMetric;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateDetails;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateProviderMock;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificateRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentActivity;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.certificate.AssignedCertificate;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.integration.SecretStoreMock;
-import com.yahoo.vespa.hosted.controller.routing.EndpointConfig;
-import org.assertj.core.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalDouble;
-import java.util.stream.Stream;
-
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.devUsEast1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.perfUsEast3;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsCentral1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.stagingTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.systemTest;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author andreer
- */
-public class EndpointCertificateMaintainerTest {
-
- private final ControllerTester tester = new ControllerTester();
- private final SecretStoreMock secretStore = (SecretStoreMock) tester.controller().secretStore();
- private final EndpointCertificateMaintainer maintainer = new EndpointCertificateMaintainer(tester.controller(), Duration.ofHours(1));
- private final CertificatePoolMaintainer certificatePoolMaintainer = new CertificatePoolMaintainer(tester.controller(), new MockMetric(), Duration.ofHours(1));
- private final EndpointCertificate exampleCert = new EndpointCertificate("keyName", "certName", 0, 0, "root-request-uuid", Optional.of("leaf-request-uuid"), List.of(), "issuer", Optional.empty(), Optional.empty(), Optional.empty());
-
- @Test
- void old_and_unused_cert_is_deleted() {
- tester.curator().writeAssignedCertificate(assignedCertificate(ApplicationId.defaultId(), exampleCert));
- assertEquals(0.0, maintainer.maintain(), 0.0000001);
- assertTrue(tester.curator().readAssignedCertificate(ApplicationId.defaultId()).isEmpty());
- }
-
- @Test
- void unused_but_recently_used_cert_is_not_deleted() {
- EndpointCertificate recentlyRequestedCert = exampleCert.withLastRequested(tester.clock().instant().minusSeconds(3600).getEpochSecond());
- tester.curator().writeAssignedCertificate(assignedCertificate(ApplicationId.defaultId(), recentlyRequestedCert));
- assertEquals(0.0, maintainer.maintain(), 0.0000001);
- assertEquals(Optional.of(recentlyRequestedCert), tester.curator().readAssignedCertificate(ApplicationId.defaultId()).map(AssignedCertificate::certificate));
- }
-
- @Test
- void refreshed_certificate_is_updated() {
- EndpointCertificate recentlyRequestedCert = exampleCert.withLastRequested(tester.clock().instant().minusSeconds(3600).getEpochSecond());
- tester.curator().writeAssignedCertificate(assignedCertificate(ApplicationId.defaultId(), recentlyRequestedCert));
-
- secretStore.setSecret(exampleCert.keyName(), "foo", 1);
- secretStore.setSecret(exampleCert.certName(), "bar", 1);
-
- assertEquals(0.0, maintainer.maintain(), 0.0000001);
-
- var updatedCert = Optional.of(recentlyRequestedCert.withLastRefreshed(tester.clock().instant().getEpochSecond()).withVersion(1));
-
- assertEquals(updatedCert, tester.curator().readAssignedCertificate(ApplicationId.defaultId()).map(AssignedCertificate::certificate));
- }
-
- @Test
- void certificate_in_use_is_not_deleted() {
- var appId = ApplicationId.from("tenant", "application", "default");
-
- DeploymentTester deploymentTester = new DeploymentTester(tester);
-
- var applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
-
- DeploymentContext deploymentContext = deploymentTester.newDeploymentContext("tenant", "application", "default");
-
- deploymentContext.submit(applicationPackage).runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1);
-
- assertEquals(0.0, maintainer.maintain(), 0.0000001);
- var cert = tester.curator().readAssignedCertificate(appId).orElseThrow().certificate();
- tester.controller().serviceRegistry().endpointCertificateProvider().certificateDetails(cert.leafRequestId().get()); // cert should not be deleted, the app is deployed!
- }
-
- @Test
- void refreshed_certificate_is_discovered_and_after_four_days_deployed() {
- prepareCertificatePool(1);
-
- var instanceId = ApplicationId.from("tenant", "application", "default");
- var applicationId = TenantAndApplicationId.from(instanceId);
-
- DeploymentTester deploymentTester = new DeploymentTester(tester);
-
- var applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .container("default")
- .build();
-
- DeploymentContext deploymentContext = deploymentTester.newDeploymentContext("tenant", "application", "default");
- deploymentContext.submit(applicationPackage).runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1);
- var assignedCertificate = tester.curator().readAssignedCertificate(applicationId, Optional.empty()).orElseThrow();
-
- // cert should not be deleted, the app is deployed!
- assertEquals(0.0, maintainer.maintain(), 0.0000001);
- assertEquals(tester.curator().readAssignedCertificate(applicationId, Optional.empty()).map(c->c.certificate().rootRequestId()), Optional.of(assignedCertificate.certificate().rootRequestId()));
- tester.controller().serviceRegistry().endpointCertificateProvider().certificateDetails(assignedCertificate.certificate().rootRequestId());
- // TODO: Remove this line when we have removed assignment of randomized id to application certificates
- //assignedCertificate = tester.curator().readAssignedCertificate().orElseThrow();
-
- // This simulates a cert refresh performed 3 days later
- tester.clock().advance(Duration.ofDays(3));
- secretStore.setSecret(assignedCertificate.certificate().keyName(), "foo", 1);
- secretStore.setSecret(assignedCertificate.certificate().certName(), "bar", 1);
- tester.controller().serviceRegistry().endpointCertificateProvider().requestCaSignedCertificate("preprovisioned." + assignedCertificate.certificate().generatedId().get(), assignedCertificate.certificate().requestedDnsSans(), Optional.of(assignedCertificate.certificate()), "rsa_2048", false);
-
- // We should now pick up the new key and cert version + uuid, but not force trigger deployment yet
- assertEquals(0.0, maintainer.maintain(), 0.0000001);
- deploymentContext.assertNotRunning(productionUsWest1);
- var updatedCert = tester.curator().readAssignedCertificate(applicationId, Optional.empty()).orElseThrow().certificate();
- assertNotEquals(assignedCertificate.certificate().leafRequestId().orElseThrow(), updatedCert.leafRequestId().orElseThrow());
- assertEquals(updatedCert.version(), assignedCertificate.certificate().version() + 1);
-
- // after another 4 days, we should force trigger deployment if it hasn't already happened
- tester.clock().advance(Duration.ofDays(4).plusSeconds(1));
- deploymentContext.assertNotRunning(productionUsWest1);
- assertEquals(0.0, maintainer.maintain(), 0.0000001);
- deploymentContext.assertRunning(productionUsWest1);
- }
-
- @Test
- void testEligibleSorting() {
- EndpointCertificateMaintainer.EligibleJob oldestDeployment = makeDeploymentAtAge(5);
- assertEquals(
- oldestDeployment,
- Stream.of(makeDeploymentAtAge(2), oldestDeployment, makeDeploymentAtAge(4)).min(maintainer.oldestFirst).get());
- }
-
- private EndpointCertificateMaintainer.EligibleJob makeDeploymentAtAge(int ageInDays) {
- var deployment = new Deployment(ZoneId.defaultId(), CloudAccount.empty, RevisionId.forProduction(1), Version.emptyVersion,
- Instant.now().minus(ageInDays, ChronoUnit.DAYS), DeploymentMetrics.none, DeploymentActivity.none, QuotaUsage.none, OptionalDouble.empty(), Map.of());
- return new EndpointCertificateMaintainer.EligibleJob(deployment, ApplicationId.defaultId(), JobType.prod("somewhere"));
- }
-
- @Test
- void unmaintained_cert_is_deleted() {
- EndpointCertificateProviderMock endpointCertificateProvider = (EndpointCertificateProviderMock) tester.controller().serviceRegistry().endpointCertificateProvider();
-
- var cert = endpointCertificateProvider.requestCaSignedCertificate("something", List.of("a", "b", "c"), Optional.empty(), "rsa_2048", false);// Unknown to controller!
-
- assertEquals(0.0, maintainer.maintain(), 0.0000001);
-
- assertTrue(endpointCertificateProvider.dnsNamesOf(cert.rootRequestId()).isEmpty());
- assertTrue(endpointCertificateProvider.listCertificates().isEmpty());
- }
-
- @Test
- void cert_pool_is_not_deleted() {
- EndpointCertificateProviderMock endpointCertificateProvider = (EndpointCertificateProviderMock) tester.controller().serviceRegistry().endpointCertificateProvider();
-
- tester.flagSource().withIntFlag(PermanentFlags.CERT_POOL_SIZE.id(), 3);
- assertEquals(0.0, certificatePoolMaintainer.maintain(), 0.0000001);
- assertEquals(0.0, maintainer.maintain(), 0.0000001);
-
- assertNotEquals(List.of(), endpointCertificateProvider.listCertificates());
- }
-
- @Test
- void deploy_to_other_manual_zone_refreshes_cert() {
- String devSan = "*.foo.manual.tenant.us-east-1.dev.vespa.oath.cloud";
- String perfSan = "*.foo.manual.tenant.us-east-3.perf.vespa.oath.cloud";
-
- var devApp = ApplicationId.from("tenant", "manual", "foo");
- DeploymentTester deploymentTester = new DeploymentTester(tester);
- deployToAssignCert(deploymentTester, devApp, List.of(devUsEast1), Optional.empty());
- assertEquals(1, tester.curator().readAssignedCertificates().size());
- maintainer.maintain();
- Optional<AssignedCertificate> devCertificate = tester.curator().readAssignedCertificate(TenantAndApplicationId.from(devApp), Optional.of(devApp.instance()));
- List<String> devSans = devCertificate.get().certificate().requestedDnsSans();
- Assertions.assertThat(devSans).contains(devSan);
- Assertions.assertThat(devSans).doesNotContain(perfSan);
-
- // Deploy to perf and verify that the certs are refreshed
- deployToAssignCert(deploymentTester, devApp, List.of(perfUsEast3), Optional.empty());
- Optional<AssignedCertificate> devAndPerfCertificate = tester.curator().readAssignedCertificate(TenantAndApplicationId.from(devApp), Optional.of(devApp.instance()));
- List<String> devAndPerfSans = devAndPerfCertificate.get().certificate().requestedDnsSans();
-
- assertNotEquals(devSans, devAndPerfSans);
- Assertions.assertThat(devAndPerfSans).contains(devSan);
- Assertions.assertThat(devAndPerfSans).contains(perfSan);
- }
-
- @Test
- void deploy_to_other_prod_zone_refreshes_cert() {
- String westSan = "*.prod.tenant.us-west-1.vespa.oath.cloud";
- String centralSan = "*.prod.tenant.us-central-1.vespa.oath.cloud";
-
- var prodApp = ApplicationId.from("tenant", "prod", "default");
- DeploymentTester deploymentTester = new DeploymentTester(tester);
- deployToAssignCert(deploymentTester, prodApp, List.of(systemTest, stagingTest, productionUsWest1), Optional.empty());
- assertEquals(1, tester.curator().readAssignedCertificates().size());
- maintainer.maintain();
- Optional<AssignedCertificate> usWestCert = tester.curator().readAssignedCertificate(TenantAndApplicationId.from(prodApp), Optional.of(prodApp.instance()));
- List<String> usWestSans = usWestCert.get().certificate().requestedDnsSans();
- Assertions.assertThat(usWestSans).contains(westSan);
- Assertions.assertThat(usWestSans).doesNotContain(centralSan);
-
- // Deploy to perf and verify that the certs are refreshed
- deployToAssignCert(deploymentTester, prodApp, List.of(systemTest, stagingTest, productionUsWest1, productionUsCentral1), Optional.empty());
- Optional<AssignedCertificate> usCentralWestCert = tester.curator().readAssignedCertificate(TenantAndApplicationId.from(prodApp), Optional.of(prodApp.instance()));
- List<String> usCentralWestSans = usCentralWestCert.get().certificate().requestedDnsSans();
- assertNotEquals(usWestSans, usCentralWestSans);
- Assertions.assertThat(usCentralWestSans).contains(westSan);
- Assertions.assertThat(usCentralWestSans).contains(centralSan);
- }
-
- private void deployToAssignCert(DeploymentTester tester, ApplicationId applicationId, List<JobType> jobTypes, Optional<String> instances) {
-
- var applicationPackageBuilder = new ApplicationPackageBuilder();
- jobTypes.stream().filter(JobType::isProduction).map(job -> job.zone().region().value()).forEach(applicationPackageBuilder::region);
-
- instances.map(applicationPackageBuilder::instances);
- var applicationPackage = applicationPackageBuilder.build();
-
- List<JobType> manualJobs = jobTypes.stream().filter(jt -> jt.environment().isManuallyDeployed()).toList();
- List<JobType> jobs = jobTypes.stream().filter(jt -> ! jt.environment().isManuallyDeployed()).toList();
-
- DeploymentContext deploymentContext = tester.newDeploymentContext(applicationId);
- deploymentContext.submit(applicationPackage);
- manualJobs.forEach(job -> deploymentContext.runJob(job, applicationPackage));
- jobs.forEach(deploymentContext::runJob);
-
- }
-
- private static AssignedCertificate assignedCertificate(ApplicationId instance, EndpointCertificate certificate) {
- return new AssignedCertificate(TenantAndApplicationId.from(instance), Optional.of(instance.instance()), certificate, false);
- }
-
- private void prepareCertificatePool(int numCertificates) {
- ((InMemoryFlagSource) tester.controller().flagSource()).withIntFlag(PermanentFlags.CERT_POOL_SIZE.id(), numCertificates);
- ((InMemoryFlagSource) tester.controller().flagSource()).withStringFlag(Flags.ENDPOINT_CONFIG.id(), EndpointConfig.generated.name());
-
- // Provision certificates
- for (int i = 0; i < numCertificates; i++) {
- certificatePoolMaintainer.maintain();
- }
-
- // Make certificate ready
- EndpointCertificateProviderMock endpointCertificateProvider = (EndpointCertificateProviderMock) tester.controller().serviceRegistry().endpointCertificateProvider();
- List<EndpointCertificateRequest> endpointCertificateRequests = endpointCertificateProvider.listCertificates();
- endpointCertificateRequests.forEach(cert -> {
- EndpointCertificateDetails details = endpointCertificateProvider.certificateDetails(cert.requestId());
- secretStore.setSecret(details.privateKeyKeyname(), "foo", 0);
- secretStore.setSecret(details.certKeyKeyname(), "bar", 0);
- });
- certificatePoolMaintainer.maintain();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/HostInfoUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/HostInfoUpdaterTest.java
deleted file mode 100644
index 583800caefa..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/HostInfoUpdaterTest.java
+++ /dev/null
@@ -1,129 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.entity.NodeEntity;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- * @author bjormel
- */
-public class HostInfoUpdaterTest {
-
- @Test
- void maintain() {
- ControllerTester tester = new ControllerTester();
- tester.serviceRegistry().configServer().nodeRepository().allowPatching(true);
- addNodeEntities(tester);
-
- // First iteration patches all hosts
- HostInfoUpdater maintainer = new HostInfoUpdater(tester.controller(), Duration.ofDays(1));
- maintainer.maintain();
- List<Node> nodes = allNodes(tester);
- assertFalse(nodes.isEmpty());
- for (var node : nodes) {
- assertEquals(node.type().isHost(), node.switchHostname().isPresent(), "Node " + node.hostname().value() + (node.type().isHost() ? " has" : " does not have")
- + " switch hostname");
- if (node.type().isHost()) {
- assertEquals("tor-" + node.hostname().value(), node.switchHostname().get());
- }
- }
-
- // Second iteration does not patch anything as all switch information is current
- tester.serviceRegistry().configServer().nodeRepository().allowPatching(false);
- maintainer.maintain();
-
- // One host is moved to a different switch
- Node host = allNodes(tester).stream().filter(node -> node.type().isHost()).findFirst().get();
- String newSwitch = "tor2-" + host.hostname().value();
- NodeEntity nodeEntity = new NodeEntity(host.hostname().value(), "RD350G", "Lenovo", newSwitch);
- tester.serviceRegistry().entityService().addNodeEntity(nodeEntity);
-
- // Host is updated
- tester.serviceRegistry().configServer().nodeRepository().allowPatching(true);
- maintainer.maintain();
- assertEquals(newSwitch, getNode(host.hostname(), tester).switchHostname().get());
-
- // Host has updated model
- String newModel = "Quanta q801";
- String manufacturer = "quanta computer";
- nodeEntity = new NodeEntity(host.hostname().value(), newModel, manufacturer, newSwitch);
- tester.serviceRegistry().entityService().addNodeEntity(nodeEntity);
-
- // Host is updated
- tester.serviceRegistry().configServer().nodeRepository().allowPatching(true);
- maintainer.maintain();
- assertEquals(manufacturer + " " + newModel, getNode(host.hostname(), tester).modelName().get());
-
- // Host keeps old switch hostname if removed from the node entity
- nodeEntity = new NodeEntity(host.hostname().value(), newModel, manufacturer, "");
- tester.serviceRegistry().entityService().addNodeEntity(nodeEntity);
- maintainer.maintain();
- assertEquals(newSwitch, getNode(host.hostname(), tester).switchHostname().get());
-
- // Host keeps old model name if removed from the node entity
- nodeEntity = new NodeEntity(host.hostname().value(), "", "", newSwitch);
- tester.serviceRegistry().entityService().addNodeEntity(nodeEntity);
- maintainer.maintain();
- assertEquals(manufacturer + " " + newModel, getNode(host.hostname(), tester).modelName().get());
-
- // Updates node registered under a different hostname
- ZoneId zone = tester.zoneRegistry().zones().controllerUpgraded().all().ids().get(0);
- String hostnameSuffix = ".prod." + zone.value();
- Node configNode = Node.builder().hostname(HostName.of("cfg3" + hostnameSuffix))
- .type(NodeType.config)
- .build();
- Node configHost = Node.builder().hostname(HostName.of("cfghost3" + hostnameSuffix))
- .type(NodeType.confighost)
- .build();
- tester.serviceRegistry().configServer().nodeRepository().putNodes(zone, List.of(configNode, configHost));
- String switchHostname = switchHostname(configHost);
- NodeEntity configNodeEntity = new NodeEntity("cfg3" + hostnameSuffix, "RD350G", "Lenovo", switchHostname);
- tester.serviceRegistry().entityService().addNodeEntity(configNodeEntity);
- maintainer.maintain();
- assertEquals(switchHostname, getNode(configHost.hostname(), tester).switchHostname().get());
- assertTrue(getNode(configNode.hostname(), tester).switchHostname().isEmpty(), "Switch hostname is not set for non-host");
- }
-
- private static Node getNode(HostName hostname, ControllerTester tester) {
- return allNodes(tester).stream()
- .filter(node -> node.hostname().equals(hostname))
- .findFirst()
- .orElseThrow(() -> new IllegalArgumentException("No such node: " + hostname));
- }
-
- private static List<Node> allNodes(ControllerTester tester) {
- List<Node> nodes = new ArrayList<>();
- for (var zone : tester.zoneRegistry().zones().controllerUpgraded().all().ids()) {
- nodes.addAll(tester.serviceRegistry().configServer().nodeRepository().list(zone, NodeFilter.all()));
- }
- return nodes;
- }
-
- private static String switchHostname(Node node) {
- return "tor-" + node.hostname().value();
- }
-
- private static void addNodeEntities(ControllerTester tester) {
- for (var node : allNodes(tester)) {
- if (!node.type().isHost()) continue;
- NodeEntity nodeEntity = new NodeEntity(node.hostname().value(), "RD350G", "Lenovo", switchHostname(node));
- tester.serviceRegistry().entityService().addNodeEntity(nodeEntity);
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
deleted file mode 100644
index d96de8df6fd..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/JobRunnerTest.java
+++ /dev/null
@@ -1,581 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import ai.vespa.metrics.ControllerMetrics;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.jdisc.test.MockMetric;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.deployment.JobController;
-import com.yahoo.vespa.hosted.controller.deployment.JobMetrics;
-import com.yahoo.vespa.hosted.controller.deployment.JobProfile;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.Run.Reason;
-import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-import com.yahoo.vespa.hosted.controller.deployment.Step.Status;
-import com.yahoo.vespa.hosted.controller.deployment.StepRunner;
-import com.yahoo.vespa.hosted.controller.deployment.Submission;
-import com.yahoo.vespa.hosted.controller.deployment.Versions;
-import com.yahoo.vespa.hosted.controller.integration.MetricsMock;
-import com.yahoo.vespa.hosted.controller.maintenance.JobRunner.Metrics;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Queue;
-import java.util.Set;
-import java.util.concurrent.AbstractExecutorService;
-import java.util.concurrent.BrokenBarrierException;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.CyclicBarrier;
-import java.util.concurrent.Executor;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.Phaser;
-import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.stagingTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.systemTest;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.aborted;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.error;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.reset;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.success;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.testFailure;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.failed;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.copyVespaLogs;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.endTests;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.report;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.startTests;
-import static java.util.Objects.requireNonNull;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotSame;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author jonmv
- */
-public class JobRunnerTest {
-
- private static final ApplicationPackage applicationPackage = new ApplicationPackage(new byte[0]);
- private static final Versions versions = new Versions(Version.fromString("1.2.3"),
- RevisionId.forProduction(321),
- Optional.empty(),
- Optional.empty());
-
- @Test
- void multiThreadedExecutionFinishes() {
- DeploymentTester tester = new DeploymentTester();
- JobController jobs = tester.controller().jobController();
- StepRunner stepRunner = (step, id) -> id.type().equals(stagingTest) && step.get() == startTests ? Optional.of(error) : Optional.of(running);
- Phaser phaser = new Phaser(1);
- JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), phasedExecutor(phaser), stepRunner);
-
- TenantAndApplicationId appId = tester.createApplication("tenant", "real", "default").id();
- ApplicationId id = appId.defaultInstance();
- byte[] testPackageBytes = new byte[0];
- jobs.submit(appId, submission(applicationPackage, testPackageBytes), 2);
- start(jobs, id, systemTest);
- try {
- start(jobs, id, systemTest);
- fail("Job is already running, so this should not be allowed!");
- }
- catch (IllegalArgumentException ignored) {
- }
- start(jobs, id, stagingTest);
-
- assertTrue(jobs.last(id, systemTest).get().stepStatuses().values().stream().allMatch(unfinished::equals));
- assertFalse(jobs.last(id, systemTest).get().hasEnded());
- assertTrue(jobs.last(id, stagingTest).get().stepStatuses().values().stream().allMatch(unfinished::equals));
- assertFalse(jobs.last(id, stagingTest).get().hasEnded());
-
- runner.maintain();
- phaser.arriveAndAwaitAdvance();
- assertTrue(jobs.last(id, systemTest).get().stepStatuses().values().stream().allMatch(succeeded::equals));
- assertTrue(jobs.last(id, stagingTest).get().hasFailed());
-
- runner.maintain();
- phaser.arriveAndAwaitAdvance();
- assertTrue(jobs.last(id, systemTest).get().hasEnded());
- assertTrue(jobs.last(id, stagingTest).get().hasEnded());
- }
-
- @Test
- void metrics() {
- Phaser phaser = new Phaser(4);
- StepRunner runner = (step, id) -> {
- phaser.arriveAndAwaitAdvance();
- phaser.arriveAndAwaitAdvance();
- return Optional.of(running);
- };
- ExecutorService executor = new ThreadPoolExecutor(3, 3, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), (task, pool) -> task.run());
- DeploymentTester tester = new DeploymentTester();
- MockMetric metric = new MockMetric();
- Metrics metrics = new Metrics(metric, Duration.ofDays(1));
- JobRunner jobs = new JobRunner(tester.controller(), Duration.ofDays(1), executor, runner, metrics);
- tester.newDeploymentContext().submit();
-
- assertEquals(Map.of(), metric.metrics());
- metrics.report();
- assertEquals(Map.of(ControllerMetrics.DEPLOYMENT_JOBS_QUEUED.baseName(),
- Map.of(Map.of(), 0.0),
- ControllerMetrics.DEPLOYMENT_JOBS_ACTIVE.baseName(),
- Map.of(Map.of(), 0.0)),
- metric.metrics());
- tester.triggerJobs();
-
- assertEquals(2, tester.jobs().active().size());
- jobs.maintain();
- phaser.arriveAndAwaitAdvance();
- metrics.report();
- assertEquals(Map.of(ControllerMetrics.DEPLOYMENT_JOBS_QUEUED.baseName(),
- Map.of(Map.of(), 1.0),
- ControllerMetrics.DEPLOYMENT_JOBS_ACTIVE.baseName(),
- Map.of(Map.of(), 3.0)),
- metric.metrics());
-
- jobs.shutdown();
- phaser.forceTermination();
- jobs.awaitShutdown();
- metrics.report();
- assertEquals(Map.of(ControllerMetrics.DEPLOYMENT_JOBS_QUEUED.baseName(),
- Map.of(Map.of(), 0.0),
- ControllerMetrics.DEPLOYMENT_JOBS_ACTIVE.baseName(),
- Map.of(Map.of(), 0.0)),
- metric.metrics());
- }
-
- @Test
- void stepLogic() {
- DeploymentTester tester = new DeploymentTester();
- JobController jobs = tester.controller().jobController();
- Map<Step, RunStatus> outcomes = new EnumMap<>(Step.class);
- JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), inThreadInOrderExecutor(), mappedRunner(outcomes));
-
- TenantAndApplicationId appId = tester.createApplication("tenant", "real", "default").id();
- ApplicationId id = appId.defaultInstance();
- byte[] testPackageBytes = new byte[0];
- jobs.submit(appId, submission(applicationPackage, testPackageBytes), 2);
- Supplier<Run> run = () -> jobs.last(id, systemTest).get();
-
- start(jobs, id, systemTest);
- RunId first = run.get().id();
-
- Map<Step, Status> steps = run.get().stepStatuses();
- runner.maintain();
- assertEquals(steps, run.get().stepStatuses());
- assertEquals(List.of(deployTester, deployReal), run.get().readySteps());
- assertStepsWithStartTime(run.get(), deployTester, deployReal);
-
- outcomes.put(deployTester, running);
- runner.maintain();
- assertEquals(List.of(installTester, deployReal), run.get().readySteps());
- assertStepsWithStartTime(run.get(), installTester, deployTester, deployReal);
-
- outcomes.put(deployReal, running);
- runner.maintain();
- assertEquals(List.of(installTester, installReal), run.get().readySteps());
- assertStepsWithStartTime(run.get(), deployTester, deployReal, installTester, installReal);
-
- outcomes.put(installReal, running);
- runner.maintain();
- assertEquals(List.of(installTester), run.get().readySteps());
- assertStepsWithStartTime(run.get(), deployTester, deployReal, installTester, installReal);
-
- outcomes.put(installTester, running);
- runner.maintain();
- assertEquals(List.of(startTests), run.get().readySteps());
- assertStepsWithStartTime(run.get(), deployTester, deployReal, installTester, installReal, startTests);
-
- outcomes.put(startTests, running);
- runner.maintain();
- assertEquals(List.of(endTests), run.get().readySteps());
- assertStepsWithStartTime(run.get(), deployTester, deployReal, installTester, installReal, startTests, endTests);
-
- // Failure ending tests fails the run, but run-always steps continue.
- outcomes.put(endTests, testFailure);
- runner.maintain();
- assertTrue(run.get().hasFailed());
- assertEquals(List.of(copyVespaLogs), run.get().readySteps());
- assertStepsWithStartTime(run.get(), deployTester, deployReal, installTester, installReal, startTests, endTests, copyVespaLogs);
-
- outcomes.put(copyVespaLogs, running);
- runner.maintain();
- assertEquals(List.of(deactivateReal, deactivateTester), run.get().readySteps());
- assertStepsWithStartTime(run.get(), deployTester, deployReal, installTester, installReal, startTests, endTests, copyVespaLogs, deactivateTester, deactivateReal);
-
- // Abortion does nothing, as the run has already failed.
- jobs.abort(run.get().id(), "abort", false);
- runner.maintain();
- assertEquals(List.of(deactivateReal, deactivateTester), run.get().readySteps());
- assertStepsWithStartTime(run.get(), deployTester, deployReal, installTester, installReal, startTests, endTests, copyVespaLogs, deactivateTester, deactivateReal);
-
- outcomes.put(deactivateReal, running);
- outcomes.put(deactivateTester, running);
- outcomes.put(report, running);
- runner.maintain();
- assertTrue(run.get().hasFailed());
- assertTrue(run.get().hasEnded());
- assertSame(aborted, run.get().status());
-
- // A new run is attempted.
- start(jobs, id, systemTest);
- assertEquals(first.number() + 1, run.get().id().number());
-
- // Run fails on tester deployment -- remaining run-always steps succeed, and the run finishes.
- outcomes.put(deployTester, error);
- runner.maintain();
- assertTrue(run.get().hasEnded());
- assertTrue(run.get().hasFailed());
- assertNotSame(aborted, run.get().status());
- assertEquals(failed, run.get().stepStatuses().get(deployTester));
- assertEquals(unfinished, run.get().stepStatuses().get(installTester));
- assertEquals(succeeded, run.get().stepStatuses().get(report));
- // deployTester, plus all forced steps:
- assertStepsWithStartTime(run.get(), deployTester, copyVespaLogs, deactivateTester, deactivateReal, report);
-
- assertEquals(2, jobs.runs(id, systemTest).size());
-
- // Start a third run, then unregister and wait for data to be deleted.
- start(jobs, id, systemTest);
- tester.applications().deleteInstance(id);
- runner.maintain();
- assertFalse(jobs.last(id, systemTest).isPresent());
- assertTrue(jobs.runs(id, systemTest).isEmpty());
- }
-
- private void assertStepsWithStartTime(Run lastRun, Step... stepsWithStartTime) {
- Set<Step> actualStepsWithStartTime = lastRun.steps().entrySet().stream()
- .filter(entry -> entry.getValue().startTime().isPresent())
- .map(Map.Entry::getKey)
- .collect(Collectors.toSet());
- assertEquals(Set.of(stepsWithStartTime), actualStepsWithStartTime);
- }
-
- @Test
- void locksAndGarbage() throws InterruptedException, BrokenBarrierException {
- DeploymentTester tester = new DeploymentTester();
- JobController jobs = tester.controller().jobController();
- // Hang during tester deployment, until notified.
- CyclicBarrier barrier = new CyclicBarrier(2);
- JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), Executors.newFixedThreadPool(32), waitingRunner(barrier));
-
- TenantAndApplicationId appId = tester.createApplication("tenant", "real", "default").id();
- ApplicationId id = appId.defaultInstance();
- byte[] testPackageBytes = new byte[0];
- jobs.submit(appId, submission(applicationPackage, testPackageBytes), 2);
-
- RunId runId = new RunId(id, systemTest, 1);
- start(jobs, id, systemTest);
- runner.maintain();
- barrier.await();
- try {
- jobs.locked(id, systemTest, deactivateTester, step -> {
- });
- fail("deployTester step should still be locked!");
- }
- catch (TimeoutException ignored) {
- }
-
- // Thread is still trying to deploy tester -- delete application, and see all data is garbage collected.
- assertEquals(Collections.singletonList(runId), jobs.active().stream().map(run -> run.id()).toList());
- tester.controllerTester().controller().applications().deleteApplication(TenantAndApplicationId.from(id), tester.controllerTester().credentialsFor(id.tenant()));
- assertEquals(Collections.emptyList(), jobs.active());
- assertEquals(runId, jobs.last(id, systemTest).get().id());
-
- // Deployment still ongoing, so garbage is not yet collected.
- runner.maintain();
- assertEquals(runId, jobs.last(id, systemTest).get().id());
-
- // Deployment lets go, deactivation may now run, and trash is thrown out.
- barrier.await();
- runner.maintain();
- assertEquals(Optional.empty(), jobs.last(id, systemTest));
- }
-
- @Test
- void historyPruning() {
- DeploymentTester tester = new DeploymentTester();
- JobController jobs = tester.controller().jobController();
- JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), inThreadInOrderExecutor(), (id, step) -> Optional.of(running));
-
- TenantAndApplicationId appId = tester.createApplication("tenant", "real", "default").id();
- ApplicationId instanceId = appId.defaultInstance();
- JobId jobId = new JobId(instanceId, systemTest);
- byte[] testPackageBytes = new byte[0];
- jobs.submit(appId, submission(applicationPackage, testPackageBytes), 2);
- assertFalse(jobs.lastSuccess(jobId).isPresent());
-
- for (int i = 0; i < jobs.historyLength(); i++) {
- start(jobs, instanceId, systemTest);
- runner.run();
- }
-
- assertEquals(64, jobs.runs(jobId).size());
- assertTrue(jobs.details(new RunId(instanceId, systemTest, 1)).isPresent());
-
- start(jobs, instanceId, systemTest);
- runner.run();
-
- assertEquals(64, jobs.runs(jobId).size());
- assertEquals(2, jobs.runs(jobId).keySet().iterator().next().number());
- assertFalse(jobs.details(new RunId(instanceId, systemTest, 1)).isPresent());
- assertTrue(jobs.details(new RunId(instanceId, systemTest, 65)).isPresent());
-
- JobRunner failureRunner = new JobRunner(tester.controller(), Duration.ofDays(1), inThreadInOrderExecutor(), (id, step) -> Optional.of(error));
-
- // Make all but the oldest of the 54 jobs a failure.
- for (int i = 0; i < jobs.historyLength() - 1; i++) {
- start(jobs, instanceId, systemTest);
- failureRunner.run();
- }
- assertEquals(64, jobs.runs(jobId).size());
- assertEquals(65, jobs.runs(jobId).keySet().iterator().next().number());
- assertEquals(65, jobs.lastSuccess(jobId).get().id().number());
- assertEquals(66, jobs.firstFailing(jobId).get().id().number());
-
- // Oldest success is kept even though it would normally overflow.
- start(jobs, instanceId, systemTest);
- failureRunner.run();
- assertEquals(65, jobs.runs(jobId).size());
- assertEquals(65, jobs.runs(jobId).keySet().iterator().next().number());
- assertEquals(65, jobs.lastSuccess(jobId).get().id().number());
- assertEquals(66, jobs.firstFailing(jobId).get().id().number());
-
- // First failure after the last success is also kept.
- start(jobs, instanceId, systemTest);
- failureRunner.run();
- assertEquals(66, jobs.runs(jobId).size());
- assertEquals(65, jobs.runs(jobId).keySet().iterator().next().number());
- assertEquals(66, jobs.runs(jobId).keySet().stream().skip(1).iterator().next().number());
- assertEquals(65, jobs.lastSuccess(jobId).get().id().number());
- assertEquals(66, jobs.firstFailing(jobId).get().id().number());
-
- // No other jobs are kept with repeated failures.
- start(jobs, instanceId, systemTest);
- failureRunner.run();
- assertEquals(66, jobs.runs(jobId).size());
- assertEquals(65, jobs.runs(jobId).keySet().iterator().next().number());
- assertEquals(66, jobs.runs(jobId).keySet().stream().skip(1).iterator().next().number());
- assertEquals(68, jobs.runs(jobId).keySet().stream().skip(2).iterator().next().number());
- assertEquals(65, jobs.lastSuccess(jobId).get().id().number());
- assertEquals(66, jobs.firstFailing(jobId).get().id().number());
-
- // history length returns to 256 when a new success is recorded.
- start(jobs, instanceId, systemTest);
- runner.run();
- assertEquals(64, jobs.runs(jobId).size());
- assertEquals(69, jobs.runs(jobId).keySet().iterator().next().number());
- assertEquals(132, jobs.lastSuccess(jobId).get().id().number());
- assertFalse(jobs.firstFailing(jobId).isPresent());
- }
-
- @Test
- void onlySuccessfulRunExpiresThenAnotherFails() {
- DeploymentTester tester = new DeploymentTester();
- JobController jobs = tester.controller().jobController();
- var app = tester.newDeploymentContext().submit();
- JobId jobId = new JobId(app.instanceId(), systemTest);
- assertFalse(jobs.lastSuccess(jobId).isPresent());
-
- app.runJob(systemTest);
- assertTrue(jobs.lastSuccess(jobId).isPresent());
- assertEquals(1, jobs.runs(jobId).size());
-
- tester.clock().advance(JobController.maxHistoryAge.plusSeconds(1));
- app.submit();
- app.failDeployment(systemTest);
- assertFalse(jobs.lastSuccess(jobId).isPresent());
- assertEquals(1, jobs.runs(jobId).size());
- }
-
- @Test
- void timeout() {
- DeploymentTester tester = new DeploymentTester();
- JobController jobs = tester.controller().jobController();
- Map<Step, RunStatus> outcomes = new EnumMap<>(Step.class);
- JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), inThreadInOrderExecutor(), mappedRunner(outcomes));
-
- TenantAndApplicationId appId = tester.createApplication("tenant", "real", "default").id();
- ApplicationId id = appId.defaultInstance();
- byte[] testPackageBytes = new byte[0];
- jobs.submit(appId, submission(applicationPackage, testPackageBytes), 2);
-
- start(jobs, id, systemTest);
- tester.clock().advance(JobRunner.jobTimeout.plus(Duration.ofSeconds(1)));
- runner.run();
- assertSame(aborted, jobs.last(id, systemTest).get().status());
- }
-
- @Test
- void jobMetrics() throws TimeoutException {
- DeploymentTester tester = new DeploymentTester();
- JobController jobs = tester.controller().jobController();
- Map<Step, RunStatus> outcomes = new EnumMap<>(Step.class);
- JobRunner runner = new JobRunner(tester.controller(), Duration.ofDays(1), inThreadInOrderExecutor(), mappedRunner(outcomes));
-
- TenantAndApplicationId appId = tester.createApplication("tenant", "real", "default").id();
- ApplicationId id = appId.defaultInstance();
- byte[] testPackageBytes = new byte[0];
- jobs.submit(appId, submission(applicationPackage, testPackageBytes), 2);
-
- for (Step step : JobProfile.of(systemTest).steps())
- outcomes.put(step, running);
-
- for (RunStatus status : RunStatus.values()) {
- if (status == success || status == reset) continue; // Status not used for steps.
- outcomes.put(deployTester, status);
- start(jobs, id, systemTest);
- runner.run();
- jobs.finish(jobs.last(id, systemTest).get().id());
- }
-
- Map<String, String> context = Map.of("applicationId", "tenant.real.default",
- "tenantName", "tenant",
- "app", "real.default",
- "test", "true",
- "zone", "test.us-east-1");
- MetricsMock metric = ((MetricsMock) tester.controller().metric());
- assertEquals(RunStatus.values().length - 2, metric.getMetric(context::equals, JobMetrics.start).get().intValue());
- assertEquals(1, metric.getMetric(context::equals, JobMetrics.abort).get().intValue());
- assertEquals(1, metric.getMetric(context::equals, JobMetrics.error).get().intValue());
- assertEquals(1, metric.getMetric(context::equals, JobMetrics.success).get().intValue());
- assertEquals(1, metric.getMetric(context::equals, JobMetrics.convergenceFailure).get().intValue());
- assertEquals(1, metric.getMetric(context::equals, JobMetrics.deploymentFailure).get().intValue());
- assertEquals(1, metric.getMetric(context::equals, JobMetrics.nodeAllocationFailure).get().intValue());
- assertEquals(1, metric.getMetric(context::equals, JobMetrics.endpointCertificateTimeout).get().intValue());
- assertEquals(1, metric.getMetric(context::equals, JobMetrics.testFailure).get().intValue());
- assertEquals(1, metric.getMetric(context::equals, JobMetrics.noTests).get().intValue());
- }
-
- @Test
- void testInThreadExecutor() throws InterruptedException {
- ExecutorService executor = inThreadInOrderExecutor();
- AtomicInteger c = new AtomicInteger(0), d = new AtomicInteger(0);
- Consumer<AtomicInteger> task = i -> executor.execute(() -> {
- executor.execute(() -> {
- i.set(2);
- executor.execute(() -> i.set(4));
- });
- executor.execute(() -> i.set(3));
- i.set(1);
- });
- Thread s = new Thread(() -> task.accept(d));
- s.start();
- task.accept(c);
- s.join();
- assertEquals(4, c.get());
- assertEquals(4, d.get());
- assertEquals("executor is shut down",
- assertThrows(RejectedExecutionException.class,
- () -> executor.execute(() -> {
- executor.execute(() -> executor.execute(() -> { c.set(6); }));
- executor.shutdown();
- c.set(5);
- })).getMessage());
- assertEquals(5, c.get());
- }
-
- private void start(JobController jobs, ApplicationId id, JobType type) {
- jobs.start(id, type, versions, false, Reason.empty());
- }
-
- /** Dummy test executor for unit tests. Runs tasks BFS rather than DFS, like a simple {@code Runnable::run} would do. No real shutdown logic. */
- public static ExecutorService inThreadInOrderExecutor() {
- return new AbstractExecutorService() {
- private final ThreadLocal<Boolean> inExecute = ThreadLocal.withInitial(() -> false);
- private final ThreadLocal<Queue<Runnable>> tasks = ThreadLocal.withInitial(ConcurrentLinkedQueue::new);
- private final AtomicBoolean shutDown = new AtomicBoolean(false);
- @Override
- public void execute(Runnable command) {
- if (isShutdown()) throw new RejectedExecutionException("executor is shut down");
- tasks.get().add(requireNonNull(command));
- if (inExecute.get()) return;
- inExecute.set(true);
- try { Runnable task; while (null != (task = tasks.get().poll())) task.run(); }
- finally { inExecute.set(false); }
- }
- @Override public void shutdown() { shutDown.set(true); }
- @Override public List<Runnable> shutdownNow() { shutDown.set(true); return Collections.emptyList(); }
- @Override public boolean isShutdown() { return shutDown.get(); }
- @Override public boolean isTerminated() { return shutDown.get(); }
- @Override public boolean awaitTermination(long timeout, TimeUnit unit) { return true; }
- };
- }
-
- private static ExecutorService phasedExecutor(Phaser phaser) {
- return new AbstractExecutorService() {
- final ExecutorService delegate = Executors.newFixedThreadPool(32);
- @Override public void shutdown() { delegate.shutdown(); }
- @Override public List<Runnable> shutdownNow() { return delegate.shutdownNow(); }
- @Override public boolean isShutdown() { return delegate.isShutdown(); }
- @Override public boolean isTerminated() { return delegate.isTerminated(); }
- @Override public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { return delegate.awaitTermination(timeout, unit); }
- @Override public void execute(Runnable command) {
- phaser.register();
- delegate.execute(() -> {
- try { command.run(); }
- finally { phaser.arriveAndDeregister(); }
- });
- }
- };
- }
-
- private static StepRunner mappedRunner(Map<Step, RunStatus> outcomes) {
- return (step, id) -> Optional.ofNullable(outcomes.get(step.get()));
- }
-
- private static StepRunner waitingRunner(CyclicBarrier barrier) {
- return (step, id) -> {
- try {
- if (step.get() == deployTester) {
- barrier.await(); // Wake up the main thread, which waits for this step to be locked.
- barrier.reset();
- barrier.await(); // Then wait while holding the lock for this step, until the main thread wakes us up.
- }
- }
- catch (InterruptedException | BrokenBarrierException e) {
- throw new AssertionError(e);
- }
- return Optional.of(running);
- };
- }
-
- private static Submission submission(ApplicationPackage applicationPackage, byte[] testPackage) {
- return new Submission(applicationPackage, testPackage, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Instant.EPOCH, 0);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MeteringMonitorMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MeteringMonitorMaintainerTest.java
deleted file mode 100644
index a15deb17c0b..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MeteringMonitorMaintainerTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistryMock;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClientMock;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.integration.MetricsMock;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Map;
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author olaa
- */
-public class MeteringMonitorMaintainerTest {
-
- private ControllerTester tester;
- private DeploymentTester deploymentTester;
- private MetricsMock metrics;
- private ResourceDatabaseClientMock database;
- private MeteringMonitorMaintainer maintainer;
- private final ApplicationId applicationId = ApplicationId.from("foo", "bar", "default");
- private final ZoneId zone = ZoneId.from("prod.aws-us-east-1c");
-
- @BeforeEach
- public void setup() {
- tester = new ControllerTester(SystemName.Public);
- deploymentTester = new DeploymentTester(tester);
- metrics = new MetricsMock();
- database = new ResourceDatabaseClientMock(new PlanRegistryMock());
- maintainer = new MeteringMonitorMaintainer(tester.controller(), Duration.ofMinutes(5), database, metrics);
- }
-
- @Test
- void finds_stale_data() {
- deploymentTester.newDeploymentContext(applicationId).submit().deploy();
- maintainer.maintain();
- var now = tester.clock().instant().getEpochSecond();
- var lastSnapshot = tester.serviceRegistry().resourceDatabase().getOldestSnapshotTimestamp(Set.of()).getEpochSecond();
- assertEquals(now - lastSnapshot, metrics.getMetric(MeteringMonitorMaintainer.METERING_AGE_METRIC_NAME));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java
deleted file mode 100644
index aa5d6d23890..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/MetricsReporterTest.java
+++ /dev/null
@@ -1,685 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.UpgradePolicy;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.vespa.athenz.utils.AthenzIdentities;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.ZmsClientMock;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.integration.MetricsMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Set;
-import java.util.function.Supplier;
-import java.util.function.UnaryOperator;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.stagingTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.systemTest;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mortent
- */
-public class MetricsReporterTest {
-
- private final MetricsMock metrics = new MetricsMock();
- private final ZmsClientMock zmsClient = new ZmsClientMock(new AthenzDbMock(), AthenzIdentities.from("mock.identity"));
-
- @Test
- void audit_log_metric() {
- var tester = new ControllerTester();
-
- MetricsReporter metricsReporter = createReporter(tester.controller());
- // Log some operator actions
- HttpRequest req1 = HttpRequest.createTestRequest(
- "http://localhost:8080/zone/v2/prod/some_region/nodes/v2/state/dirty/hostname",
- com.yahoo.jdisc.http.HttpRequest.Method.PUT
- );
- req1.getJDiscRequest().setUserPrincipal(() -> "user.janedoe");
- tester.controller().auditLogger().log((req1));
- HttpRequest req2 = HttpRequest.createTestRequest(
- "http://localhost:8080/routing/v1/inactive/tenant/some_tenant/application/some_app/instance/default/environment/prod/region/some-region",
- com.yahoo.jdisc.http.HttpRequest.Method.POST
- );
- req2.getJDiscRequest().setUserPrincipal(() -> "user.johndoe");
- tester.controller().auditLogger().log((req2));
-
- // Report metrics
- metricsReporter.maintain();
- assertEquals(1, getMetric(MetricsReporter.OPERATION_PREFIX + "zone", "user.janedoe"));
- assertEquals(1, getMetric(MetricsReporter.OPERATION_PREFIX + "routing", "user.johndoe"));
-
- // Log some more operator actions
- HttpRequest req3 = HttpRequest.createTestRequest(
- "http://localhost:8080/zone/v2/prod/us-northeast-1/nodes/v2/state/dirty/le04614.ostk.bm2.prod.ca1.yahoo.com",
- com.yahoo.jdisc.http.HttpRequest.Method.PUT
- );
- req3.getJDiscRequest().setUserPrincipal(() -> "user.janedoe");
- tester.controller().auditLogger().log((req3));
- HttpRequest req4 = HttpRequest.createTestRequest(
- "http://localhost:8080/routing/v1/inactive/tenant/some_publishing/application/someindexing/instance/default/environment/prod/region/us-northeast-1",
- com.yahoo.jdisc.http.HttpRequest.Method.POST
- );
- req4.getJDiscRequest().setUserPrincipal(() -> "user.johndoe");
- tester.controller().auditLogger().log((req4));
- HttpRequest req5 = HttpRequest.createTestRequest(
- "http://localhost:8080/zone/v2/prod/us-northeast-1/nodes/v2/state/dirty/le04614.ostk.bm2.prod.ca1.yahoo.com",
- com.yahoo.jdisc.http.HttpRequest.Method.PUT
- );
- req5.getJDiscRequest().setUserPrincipal(() -> "user.johndoe");
- tester.controller().auditLogger().log((req5));
- HttpRequest req6 = HttpRequest.createTestRequest(
- "http://localhost:8080/routing/v1/inactive/tenant/some_publishing/application/someindexing/instance/default/environment/prod/region/us-northeast-1",
- com.yahoo.jdisc.http.HttpRequest.Method.POST
- );
- req6.getJDiscRequest().setUserPrincipal(() -> "user.janedoe");
- tester.controller().auditLogger().log((req6));
-
- // Report metrics
- metricsReporter.maintain();
- assertEquals(2, getMetric(MetricsReporter.OPERATION_PREFIX + "zone", "user.janedoe"));
- assertEquals(2, getMetric(MetricsReporter.OPERATION_PREFIX + "routing", "user.johndoe"));
- assertEquals(1, getMetric(MetricsReporter.OPERATION_PREFIX + "zone", "user.johndoe"));
- assertEquals(1, getMetric(MetricsReporter.OPERATION_PREFIX + "routing", "user.janedoe"));
- }
-
- @Test
- void deployment_fail_ratio() {
- var tester = new DeploymentTester();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
- MetricsReporter metricsReporter = createReporter(tester.controller());
-
- metricsReporter.maintain();
- assertEquals(0.0, metrics.getMetric(MetricsReporter.DEPLOYMENT_FAIL_METRIC));
-
- // Deploy all apps successfully
- var context1 = tester.newDeploymentContext("app1", "tenant1", "default");
- var context2 = tester.newDeploymentContext("app2", "tenant1", "default");
- var context3 = tester.newDeploymentContext("app3", "tenant1", "default");
- var context4 = tester.newDeploymentContext("app4", "tenant1", "default");
- context1.submit(applicationPackage).deploy();
- context2.submit(applicationPackage).deploy();
- context3.submit(applicationPackage).deploy();
- context4.submit(applicationPackage).deploy();
-
- metricsReporter.maintain();
- assertEquals(0.0, metrics.getMetric(MetricsReporter.DEPLOYMENT_FAIL_METRIC));
-
- // 1 app fails system-test
- context1.submit(applicationPackage)
- .triggerJobs()
- .failDeployment(systemTest);
-
- metricsReporter.maintain();
- assertEquals(25.0, metrics.getMetric(MetricsReporter.DEPLOYMENT_FAIL_METRIC));
- }
-
- @Test
- void deployment_average_duration() {
- var tester = new DeploymentTester();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
-
- MetricsReporter reporter = createReporter(tester.controller());
-
- var context = tester.newDeploymentContext()
- .submit(applicationPackage)
- .deploy();
- reporter.maintain();
- assertEquals(Duration.ZERO, getAverageDeploymentDuration(context.instanceId())); // An exceptionally fast deployment :-)
-
- // App spends 3 hours deploying
- context.submit(applicationPackage);
- tester.triggerJobs();
- tester.clock().advance(Duration.ofHours(1));
- context.runJob(systemTest);
-
- tester.clock().advance(Duration.ofMinutes(30));
- context.runJob(stagingTest);
-
- tester.triggerJobs();
- tester.clock().advance(Duration.ofMinutes(90));
- context.runJob(productionUsWest1);
- reporter.maintain();
-
- // Average time is 1 hour (system-test) + 90 minutes (staging-test runs in parallel with system-test) + 90 minutes (production) / 3 jobs
- assertEquals(Duration.ofMinutes(80), getAverageDeploymentDuration(context.instanceId()));
-
- // Another deployment starts and stalls for 12 hours
- context.submit(applicationPackage)
- .triggerJobs();
- tester.clock().advance(Duration.ofHours(12));
- reporter.maintain();
-
- assertEquals(Duration.ofHours(12) // hanging system-test
- .plus(Duration.ofHours(12)) // hanging staging-test
- .plus(Duration.ofMinutes(90)) // previous production job
- .dividedBy(3), // Total number of orchestrated jobs
- getAverageDeploymentDuration(context.instanceId()));
- }
-
- @Test
- void deployments_failing_upgrade() {
- var tester = new DeploymentTester();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
-
- MetricsReporter reporter = createReporter(tester.controller());
- var context = tester.newDeploymentContext();
-
- // Initial deployment without failures
- context.submit(applicationPackage).deploy();
- reporter.maintain();
- assertEquals(0, getDeploymentsFailingUpgrade(context.instanceId()));
-
- // Failing application change is not counted
- context.submit(applicationPackage)
- .triggerJobs()
- .failDeployment(systemTest);
- reporter.maintain();
- assertEquals(0, getDeploymentsFailingUpgrade(context.instanceId()));
-
- // Application change completes
- context.deploy();
- assertFalse(context.instance().change().hasTargets(), "Change deployed");
-
- // New versions is released and upgrade fails in test environments
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
- tester.upgrader().maintain();
- context.failDeployment(systemTest)
- .failDeployment(stagingTest);
- reporter.maintain();
- assertEquals(2, getDeploymentsFailingUpgrade(context.instanceId()));
-
- // Test and staging pass and upgrade fails in production
- context.runJob(systemTest)
- .runJob(stagingTest)
- .failDeployment(productionUsWest1);
- reporter.maintain();
- assertEquals(1, getDeploymentsFailingUpgrade(context.instanceId()));
-
- // Upgrade eventually succeeds
- context.runJob(productionUsWest1);
- assertFalse(context.instance().change().hasTargets(), "Upgrade deployed");
- reporter.maintain();
- assertEquals(0, getDeploymentsFailingUpgrade(context.instanceId()));
- }
-
- @Test
- void deployment_warnings_metric() {
- var tester = new DeploymentTester();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .region("us-east-3")
- .build();
- MetricsReporter reporter = createReporter(tester.controller());
- var context = tester.newDeploymentContext();
- tester.configServer().generateWarnings(context.deploymentIdIn(ZoneId.from("prod", "us-west-1")), 3);
- tester.configServer().generateWarnings(context.deploymentIdIn(ZoneId.from("prod", "us-west-1")), 4);
- context.submit(applicationPackage).deploy();
- reporter.maintain();
- assertEquals(4, getDeploymentWarnings(context.instanceId()));
- }
-
- @Test
- void build_time_reporting() {
- var tester = new DeploymentTester();
- var applicationPackage = new ApplicationPackageBuilder().region("us-west-1").build();
- var context = tester.newDeploymentContext()
- .submit(applicationPackage)
- .deploy();
- assertEquals(1000, context.application().revisions().get(context.lastSubmission().get()).buildTime().get().toEpochMilli());
-
- MetricsReporter reporter = createReporter(tester.controller());
- reporter.maintain();
- assertEquals(tester.clock().instant().getEpochSecond() - 1,
- getMetric(MetricsReporter.DEPLOYMENT_BUILD_AGE_SECONDS, context.instanceId()));
- }
-
- @Test
- void name_service_queue_size_metric() {
- var tester = new DeploymentTester();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "foo")
- .region("us-west-1")
- .region("us-east-3")
- .build();
- MetricsReporter reporter = createReporter(tester.controller());
- var context = tester.newDeploymentContext()
- .deferDnsUpdates();
- reporter.maintain();
- assertEquals(0, metrics.getMetric(MetricsReporter.NAME_SERVICE_REQUESTS_QUEUED).intValue(), "Queue is empty initially");
-
- context.submit(applicationPackage).deploy();
- reporter.maintain();
- assertEquals(2, metrics.getMetric(MetricsReporter.NAME_SERVICE_REQUESTS_QUEUED).intValue(), "Deployment queues name services requests");
-
- context.flushDnsUpdates();
- reporter.maintain();
- assertEquals(0, metrics.getMetric(MetricsReporter.NAME_SERVICE_REQUESTS_QUEUED).intValue(), "Queue consumed");
- }
-
- @Test
- void platform_change_duration() {
- var tester = new ControllerTester();
- var reporter = createReporter(tester.controller());
- var zone = ZoneId.from("prod.eu-west-1");
- tester.zoneRegistry().setUpgradePolicy(UpgradePolicy.builder().upgrade(ZoneApiMock.from(zone)).build());
- var systemUpgrader = new SystemUpgrader(tester.controller(), Duration.ofDays(1)
- );
- tester.configServer().bootstrap(List.of(zone), SystemApplication.configServer);
-
- // System on initial version
- var version0 = Version.fromString("7.0");
- tester.upgradeSystem(version0);
- reporter.maintain();
- var hosts = tester.configServer().nodeRepository().list(zone, NodeFilter.all().applications(SystemApplication.configServer.id()));
- assertPlatformChangeDuration(Duration.ZERO, hosts);
-
- var targets = List.of(Version.fromString("7.1"), Version.fromString("7.2"));
- for (int i = 0; i < targets.size(); i++) {
- var version = targets.get(i);
- // System starts upgrading to next version
- tester.upgradeController(version);
- reporter.maintain();
- assertPlatformChangeDuration(Duration.ZERO, hosts);
- systemUpgrader.maintain();
-
- // 30 minutes pass and nothing happens
- tester.clock().advance(Duration.ofMinutes(30));
- runAll(tester::computeVersionStatus, reporter);
- assertPlatformChangeDuration(Duration.ZERO, hosts);
-
- // 1/3 nodes upgrade within timeout
- assertEquals(version,
- getNodes(zone, hosts, tester).stream()
- .map(Node::wantedVersion)
- .min(Comparator.naturalOrder())
- .get(),
- "Wanted version is raised for all nodes");
- suspend(hosts, zone, tester);
- var firstHost = hosts.get(0);
- upgradeTo(version, List.of(firstHost), zone, tester);
-
- // 2/3 spend their budget and are reported as failures
- tester.clock().advance(Duration.ofHours(1));
- runAll(tester::computeVersionStatus, reporter);
- assertPlatformChangeDuration(Duration.ZERO, List.of(firstHost));
- assertPlatformChangeDuration(Duration.ofHours(1), hosts.subList(1, hosts.size()));
-
- // Remaining nodes eventually upgrade
- upgradeTo(version, hosts.subList(1, hosts.size()), zone, tester);
- runAll(tester::computeVersionStatus, reporter);
- assertPlatformChangeDuration(Duration.ZERO, hosts);
- assertEquals(version, tester.controller().readSystemVersion());
- assertPlatformNodeCount(hosts.size(), version);
- }
- }
-
- @Test
- void os_change_duration() {
- var tester = new ControllerTester();
- var reporter = createReporter(tester.controller());
- var zone = ZoneId.from("prod.eu-west-1");
- var cloud = CloudName.DEFAULT;
- tester.zoneRegistry().setOsUpgradePolicy(cloud, UpgradePolicy.builder().upgrade(ZoneApiMock.from(zone)).build());
- var osUpgrader = new OsUpgrader(tester.controller(), Duration.ofDays(1), CloudName.DEFAULT);
- var statusUpdater = new OsVersionStatusUpdater(tester.controller(), Duration.ofDays(1)
- );
- tester.configServer().bootstrap(List.of(zone), SystemApplication.configServerHost, SystemApplication.tenantHost);
-
- // All nodes upgrade to initial OS version
- var version0 = Version.fromString("8.0");
- tester.controller().os().upgradeTo(version0, cloud, false, false);
- osUpgrader.maintain();
- tester.configServer().setOsVersion(version0, SystemApplication.tenantHost.id(), zone);
- tester.configServer().setOsVersion(version0, SystemApplication.configServerHost.id(), zone);
- runAll(statusUpdater, reporter);
- List<Node> hosts = tester.configServer().nodeRepository().list(zone, NodeFilter.all());
- assertOsChangeDuration(Duration.ZERO, hosts);
-
- var targets = List.of(Version.fromString("8.1"), Version.fromString("8.2"));
- var allVersions = Stream.concat(Stream.of(version0), targets.stream()).collect(Collectors.toSet());
- for (int i = 0; i < targets.size(); i++) {
- var currentVersion = i == 0 ? version0 : targets.get(i - 1);
- var nextVersion = targets.get(i);
- // System starts upgrading to next OS version
- tester.controller().os().upgradeTo(nextVersion, cloud, false, false);
- runAll(osUpgrader, statusUpdater, reporter);
- assertOsChangeDuration(Duration.ZERO, hosts);
- assertOsNodeCount(hosts.size(), currentVersion);
-
- // Over 30 minutes pass and nothing happens
- tester.clock().advance(Duration.ofMinutes(30).plus(Duration.ofSeconds(1)));
- runAll(statusUpdater, reporter);
- assertOsChangeDuration(Duration.ZERO, hosts);
-
- // Nodes are told to upgrade, but do not suspend yet
- assertEquals(nextVersion,
- tester.configServer().nodeRepository().list(zone, NodeFilter.all().applications(SystemApplication.tenantHost.id())).stream()
- .map(Node::wantedOsVersion).min(Comparator.naturalOrder()).get(),
- "Wanted OS version is raised for all nodes");
- assertTrue(tester.controller().serviceRegistry().configServer()
- .nodeRepository().list(zone, NodeFilter.all()).stream()
- .noneMatch(node -> node.serviceState() == Node.ServiceState.allowedDown), "No nodes are suspended");
-
- // Another 30 minutes pass
- tester.clock().advance(Duration.ofMinutes(30));
- runAll(statusUpdater, reporter);
- assertOsChangeDuration(Duration.ZERO, hosts);
-
- // 3/6 hosts suspend
- var suspendedHosts = hosts.subList(0, 3);
- suspend(suspendedHosts, zone, tester);
- runAll(statusUpdater, reporter);
- assertOsChangeDuration(Duration.ZERO, hosts);
-
- // Two hosts spend 20 minutes upgrading
- var hostsUpgraded = suspendedHosts.subList(0, 2);
- tester.clock().advance(Duration.ofMinutes(20));
- runAll(statusUpdater, reporter);
- assertOsChangeDuration(Duration.ofMinutes(20), hostsUpgraded);
- upgradeOsTo(nextVersion, hostsUpgraded, zone, tester);
- runAll(statusUpdater, reporter);
- assertOsChangeDuration(Duration.ZERO, hostsUpgraded);
- assertOsNodeCount(hostsUpgraded.size(), nextVersion);
-
- // One host consumes budget without upgrading
- var brokenHost = suspendedHosts.get(2);
- tester.clock().advance(Duration.ofMinutes(15));
- runAll(statusUpdater, reporter);
- assertOsChangeDuration(Duration.ofMinutes(35), List.of(brokenHost));
-
- // Host eventually upgrades and is no longer reported
- upgradeOsTo(nextVersion, List.of(brokenHost), zone, tester);
- runAll(statusUpdater, reporter);
- assertOsChangeDuration(Duration.ZERO, List.of(brokenHost));
- assertOsNodeCount(hostsUpgraded.size() + 1, nextVersion);
-
- // Remaining hosts suspend and upgrade successfully
- var remainingHosts = hosts.subList(3, hosts.size());
- suspend(remainingHosts, zone, tester);
- upgradeOsTo(nextVersion, remainingHosts, zone, tester);
- runAll(statusUpdater, reporter);
- assertOsChangeDuration(Duration.ZERO, hosts);
- assertOsNodeCount(hosts.size(), nextVersion);
- assertOsNodeCount(0, currentVersion);
-
- // Dimensions used for node count metric are only known OS versions
- Set<Version> versionDimensions = metrics.getMetrics((dimensions) -> true)
- .entrySet()
- .stream()
- .filter(kv -> kv.getValue().containsKey(MetricsReporter.OS_NODE_COUNT))
- .map(kv -> kv.getKey().getDimensions())
- .map(dimensions -> dimensions.get("currentVersion"))
- .map(Version::fromString)
- .collect(Collectors.toSet());
- assertTrue(allVersions.containsAll(versionDimensions), "Reports only OS versions");
- }
- }
-
- @Test
- void broken_system_version() {
- var tester = new DeploymentTester().atMondayMorning();
- var ctx = tester.newDeploymentContext();
- var applicationPackage = new ApplicationPackageBuilder().upgradePolicy("canary").region("us-west-1").build();
-
- // Application deploys successfully on current system version
- ctx.submit(applicationPackage).deploy();
- tester.controllerTester().computeVersionStatus();
- var reporter = createReporter(tester.controller());
- reporter.maintain();
- assertEquals(VespaVersion.Confidence.high, tester.controller().readVersionStatus().systemVersion().get().confidence());
- assertEquals(0, metrics.getMetric(MetricsReporter.BROKEN_SYSTEM_VERSION));
-
- // System upgrades. Canary upgrade fails
- Version version0 = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version0);
- tester.upgrader().maintain();
- assertEquals(Change.of(version0), ctx.instance().change());
- ctx.failDeployment(stagingTest);
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.broken, tester.controller().readVersionStatus().systemVersion().get().confidence());
- reporter.maintain();
- assertEquals(1, metrics.getMetric(MetricsReporter.BROKEN_SYSTEM_VERSION));
-
- // Canary is healed and confidence is raised
- ctx.deployPlatform(version0);
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.high, tester.controller().readVersionStatus().systemVersion().get().confidence());
- reporter.maintain();
- assertEquals(0, metrics.getMetric(MetricsReporter.BROKEN_SYSTEM_VERSION));
- }
-
- @Test
- void tenant_counter() {
- var tester = new ControllerTester(SystemName.Public);
- tester.createTenant("foo", Tenant.Type.cloud);
- tester.createTenant("bar", Tenant.Type.cloud);
- tester.createTenant("fix", Tenant.Type.cloud);
- tester.controller().serviceRegistry().billingController().setPlan(TenantName.from("foo"), PlanId.from("pay-as-you-go"), false, false);
- tester.controller().serviceRegistry().billingController().setPlan(TenantName.from("bar"), PlanId.from("pay-as-you-go"), false, false);
-
- var reporter = createReporter(tester.controller());
- reporter.maintain();
-
- assertEquals(2, metrics.getMetric(d -> "pay-as-you-go".equals(d.get("plan")), MetricsReporter.TENANT_METRIC).get());
- assertEquals(1, metrics.getMetric(d -> "trial".equals(d.get("plan")), MetricsReporter.TENANT_METRIC).get());
- }
-
- @Test
- void overdue_upgrade_metric() {
- ApplicationPackage pkg = new ApplicationPackageBuilder().region("us-west-1")
- // window 1
- .blockChange(false, true, "mon-tue", "2-9", "CET")
- // window 2
- .blockChange(false, true, "mon-tue", "1-8,11-12", "CET")
- // window 3
- .blockChange(false, true, "wed-thu", "0-23", "CET")
- // window 4 (does not apply to upgrade)
- .blockChange(true, false, "mon-sun", "0-7", "CET")
- .build();
-
- Instant mondayNight = Instant.parse("2021-12-13T23:30:00.00Z");
- DeploymentTester tester = new DeploymentTester().at(mondayNight);
- MetricsReporter reporter = createReporter(tester.controller());
- DeploymentContext context = tester.newDeploymentContext();
- Supplier<Duration> metric = () -> {
- reporter.maintain();
- return Duration.ofSeconds(metrics.getMetric(context.instanceId(), MetricsReporter.DEPLOYMENT_OVERDUE_UPGRADE)
- .get().longValue());
- };
-
- // Deploy completely once
- context.submit(pkg).completeRollout();
-
- // System is upgraded, triggering upgrade of application
- tester.controllerTester().upgradeSystem(Version.fromString("6.2"));
- tester.upgrader().maintain();
-
- // Start production job for upgrade, without completing it
- context.runJob(systemTest)
- .runJob(stagingTest)
- .triggerJobs()
- .assertRunning(productionUsWest1);
- assertEquals(Duration.ZERO, metric.get(), "Upgrade is not overdue yet");
-
- // Upgrade continues into block window
- tester.clock().advance(Duration.ofHours(1)); // Tuesday at 00:30 (01:30 CET)
- assertEquals(Duration.ofHours(0).plusMinutes(30), metric.get(), "Upgrade is overdue measured relative to window 2");
-
- tester.clock().advance(Duration.ofHours(1)); // Tuesday at 01:30 (02:30 CET)
- assertEquals(Duration.ofHours(1).plusMinutes(30), metric.get(), "Upgrade is overdue measured relative to window 2");
-
- tester.clock().advance(Duration.ofHours(1)); // Tuesday at 02:30 (03:30 CET)
- assertEquals(Duration.ofHours(2).plusMinutes(30), metric.get(), "Upgrade is overdue measured relative to window 2");
-
- tester.clock().advance(Duration.ofHours(6)); // Tuesday at 08:30 (09:30 CET)
- assertEquals(Duration.ofHours(8).plusMinutes(30), metric.get(), "Upgrade is overdue measured relative to window 1");
-
- tester.clock().advance(Duration.ofHours(1)); // Tuesday at 09:30 (10:30 CET)
- assertEquals(Duration.ZERO, metric.get(), "Upgrade is no longer overdue");
-
- tester.clock().advance(Duration.ofDays(2)); // Thursday at 10:30 (11:30 CET)
- assertEquals(Duration.ofHours(34).plusMinutes(30), metric.get(), "Upgrade is overdue measure relative to window 3");
- }
-
- @Test
- void zms_quota_metrics() {
- var tester = new ControllerTester();
- var reporter = createReporter(tester.controller());
- reporter.maintain();
-
- assertEquals(0.1, metrics.getMetric(d -> "subdomains".equals(d.get("resourceType")), MetricsReporter.ZMS_QUOTA_USAGE).get());
- assertEquals(0.2, metrics.getMetric(d -> "roles".equals(d.get("resourceType")), MetricsReporter.ZMS_QUOTA_USAGE).get());
- assertEquals(0.3, metrics.getMetric(d -> "policies".equals(d.get("resourceType")), MetricsReporter.ZMS_QUOTA_USAGE).get());
- assertEquals(0.4, metrics.getMetric(d -> "services".equals(d.get("resourceType")), MetricsReporter.ZMS_QUOTA_USAGE).get());
- assertEquals(0.5, metrics.getMetric(d -> "groups".equals(d.get("resourceType")), MetricsReporter.ZMS_QUOTA_USAGE).get());
- }
-
- private void assertNodeCount(String metric, int n, Version version) {
- long nodeCount = metrics.getMetric((dimensions) -> version.toFullString().equals(dimensions.get("currentVersion")), metric)
- .stream()
- .map(Number::longValue)
- .findFirst()
- .orElseThrow(() -> new IllegalArgumentException("Expected to find metric for version " + version));
- assertEquals(n, nodeCount, "Expected number of nodes are on " + version.toFullString());
- }
-
- private void assertPlatformNodeCount(int n, Version version) {
- assertNodeCount(MetricsReporter.PLATFORM_NODE_COUNT, n, version);
- }
-
- private void assertOsNodeCount(int n, Version version) {
- assertNodeCount(MetricsReporter.OS_NODE_COUNT, n, version);
- }
-
- private void runAll(Runnable... runnables) {
- for (var r : runnables) r.run();
- }
-
- private void upgradeTo(Version version, List<Node> nodes, ZoneId zone, ControllerTester tester) {
- tester.configServer().setVersion(version, nodes, zone);
- resume(nodes, zone, tester);
- }
-
- private void upgradeOsTo(Version version, List<Node> nodes, ZoneId zone, ControllerTester tester) {
- tester.configServer().setOsVersion(version, nodes, zone);
- resume(nodes, zone, tester);
- }
-
- private void resume(List<Node> nodes, ZoneId zone, ControllerTester tester) {
- updateNodes(nodes, (builder) -> builder.serviceState(Node.ServiceState.expectedUp).suspendedSince(null),
- zone, tester);
- }
-
- private void suspend(List<Node> nodes, ZoneId zone, ControllerTester tester) {
- var now = tester.clock().instant();
- updateNodes(nodes, (builder) -> builder.serviceState(Node.ServiceState.allowedDown).suspendedSince(now),
- zone, tester);
- }
-
- private List<Node> getNodes(ZoneId zone, List<Node> nodes, ControllerTester tester) {
- return tester.configServer().nodeRepository().list(zone, NodeFilter.all().hostnames(nodes.stream()
- .map(Node::hostname)
- .collect(Collectors.toSet())));
- }
-
- private void updateNodes(List<Node> nodes, UnaryOperator<Node.Builder> builderOps, ZoneId zone,
- ControllerTester tester) {
- var currentNodes = getNodes(zone, nodes, tester);
- var updatedNodes = currentNodes.stream()
- .map(node -> builderOps.apply(Node.builder(node)).build())
- .toList();
- tester.configServer().nodeRepository().putNodes(zone, updatedNodes);
- }
-
- private Duration getAverageDeploymentDuration(ApplicationId id) {
- return Duration.ofSeconds(getMetric(MetricsReporter.DEPLOYMENT_AVERAGE_DURATION, id).longValue());
- }
-
- private int getDeploymentsFailingUpgrade(ApplicationId id) {
- return getMetric(MetricsReporter.DEPLOYMENT_FAILING_UPGRADES, id).intValue();
- }
-
- private int getDeploymentWarnings(ApplicationId id) {
- return getMetric(MetricsReporter.DEPLOYMENT_WARNINGS, id).intValue();
- }
-
- private Duration getChangeDuration(String metric, HostName hostname) {
- return metrics.getMetric((dimensions) -> hostname.value().equals(dimensions.get("host")), metric)
- .map(n -> Duration.ofSeconds(n.longValue()))
- .orElseThrow(() -> new IllegalArgumentException("Expected to find metric for " + hostname));
- }
-
- private void assertPlatformChangeDuration(Duration duration, List<Node> nodes) {
- for (var node : nodes) {
- assertEquals(duration, getChangeDuration(MetricsReporter.PLATFORM_CHANGE_DURATION, node.hostname()), "Platform change duration of " + node.hostname());
- }
- }
-
- private void assertOsChangeDuration(Duration duration, List<Node> nodes) {
- for (var node : nodes) {
- assertEquals(duration, getChangeDuration(MetricsReporter.OS_CHANGE_DURATION, node.hostname()), "OS change duration of " + node.hostname());
- }
- }
-
- private Number getMetric(String name, ApplicationId id) {
- return metrics.getMetric((dimensions) -> id.tenant().value().equals(dimensions.get("tenantName")) &&
- appDimension(id).equals(dimensions.get("app")),
- name)
- .orElseThrow(() -> new RuntimeException("Expected metric to exist for " + id));
- }
-
- private Number getMetric(String name, String operator) {
- return metrics.getMetric((dimensions) -> operator.equals(dimensions.get("operator")),
- name)
- .orElseThrow(() -> new RuntimeException("Expected metric to exist for " + operator));
- }
-
- private MetricsReporter createReporter(Controller controller) {
- return new MetricsReporter(controller, metrics, zmsClient);
- }
-
- private static String appDimension(ApplicationId id) {
- return id.application().value() + "." + id.instance().value();
- }
-
-}
-
-
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcherTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcherTest.java
deleted file mode 100644
index c0975049f63..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/NameServiceDispatcherTest.java
+++ /dev/null
@@ -1,138 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.AliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.DirectTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.NameService;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record.Type;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.dns.CreateRecord;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue.Priority;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceRequest;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author jonmv
- */
-class NameServiceDispatcherTest {
-
- @Test
- void testDispatch() {
- Deque<Consumer<RecordName>> expectations = new ArrayDeque<>();
- var nameService = new NameService() {
- @Override public Record createRecord(Type type, RecordName name, RecordData data) { expectations.pop().accept(name); return null; }
- @Override public List<Record> createAlias(RecordName name, Set<AliasTarget> targets) { throw new UnsupportedOperationException(); }
- @Override public List<Record> createDirect(RecordName name, Set<DirectTarget> targets) { throw new UnsupportedOperationException(); }
- @Override public List<Record> createTxtRecords(RecordName name, List<RecordData> txtRecords) { throw new UnsupportedOperationException(); }
- @Override public List<Record> findRecords(Type type, RecordName name) { return List.of(); }
- @Override public void updateRecord(Record record, RecordData newData) { throw new UnsupportedOperationException(); }
- @Override public void removeRecords(List<Record> record) { throw new UnsupportedOperationException(); }
- };
- ControllerTester tester = new ControllerTester();
- NameServiceDispatcher dispatcher = new NameServiceDispatcher(tester.controller(), nameService, Duration.ofMinutes(1));
-
- var owner0 = Optional.<TenantAndApplicationId>empty();
- var owner1 = Optional.of(TenantAndApplicationId.from("t", "a"));
- var owner2 = Optional.of(TenantAndApplicationId.from("t", "b"));
-
- var rec1 = new Record(Type.A, RecordName.from("one"), RecordData.from("data"));
- var rec2 = new Record(Type.A, RecordName.from("two"), RecordData.from("data"));
- var rec3 = new Record(Type.A, RecordName.from("three"), RecordData.from("data"));
- var rec4 = new Record(Type.A, RecordName.from("four"), RecordData.from("data"));
-
- var req1 = new CreateRecord(owner0, rec1);
- var req2 = new CreateRecord(owner1, rec2);
- var req3 = new CreateRecord(owner0, rec1);
- var req4 = new CreateRecord(owner1, rec4);
- var req5 = new CreateRecord(owner2, rec3);
- var req6 = new CreateRecord(owner1, rec4);
- var req7 = new CreateRecord(owner0, rec1);
- var req8 = new CreateRecord(owner0, rec3);
-
- // Queue initially contains a subsequence of requests, 3–6.
- var base = new LinkedList<NameServiceRequest>(List.of(req3, req4, req5, req6));
-
- // Whole queue is consumed by dispatcher the first time, and a few records prepended.
- tester.curator().writeNameServiceQueue(new NameServiceQueue(base));
- expectations.add(name -> assertEquals(rec1.name(), name));
- expectations.add(name -> assertEquals(rec4.name(), name));
- expectations.add(name -> assertEquals(rec3.name(), name));
- expectations.add(name -> {
- assertEquals(rec4.name(), name);
- tester.curator().writeNameServiceQueue(tester.curator().readNameServiceQueue()
- .with(req1, Priority.high)
- .with(req2, Priority.high)
- .with(req1, Priority.high));
- });
- assertEquals(1.0, dispatcher.maintain());
- assertEquals(List.of(req1, req2, req1), tester.curator().readNameServiceQueue().requests());
-
- // Now, the dispatch of requests owned by owner1 will fail, so the remaining list is subsequence or the original.
- tester.curator().writeNameServiceQueue(new NameServiceQueue(base));
- AtomicReference<Consumer<RecordName>> failOwner1 = new AtomicReference<>();
- failOwner1.set(name -> {
- assertEquals(rec4.name(), name);
- expectations.add(failOwner1.get());
- throw new RuntimeException("test error");
- });
- expectations.add(name -> assertEquals(rec1.name(), name));
- expectations.add(failOwner1.get()); // Recursively adds itself to the tail of the expectation queue.
- expectations.add(name -> assertEquals(rec3.name(), name));
- assertEquals(0.5, dispatcher.maintain()); // 2 of 4 requests were ok (the fourth was never attempted).
- assertEquals(List.of(req4, req6), tester.curator().readNameServiceQueue().requests());
-
- // Queue again initially contains a subsequence of requests, 3–6.
- // While the dispatcher is working through those, some requests are appended, and some are prepended.
- // The dispatch of requests owned by owner1 will fail, so the original sublist read by the dispatcher
- // is not removed, but replaced by a subsequence, upon dispatch end.
- tester.curator().writeNameServiceQueue(new NameServiceQueue(base));
- expectations.clear();
- expectations.add(name -> {
- assertEquals(rec1.name(), name);
- tester.curator().writeNameServiceQueue(tester.curator().readNameServiceQueue()
- .with(req7)
- .with(req2, Priority.high)
- .with(req1, Priority.high)
- .with(req8));
- assertEquals(List.of(req1, req2, req3, req4, req5, req6, req7, req8), tester.curator().readNameServiceQueue().requests());
- });
- expectations.add(failOwner1.get()); // Recursively adds itself to the tail of the expectation queue.
- expectations.add(name -> assertEquals(rec3.name(), name));
- assertEquals(0.5, dispatcher.maintain()); // 2 of 4 requests were ok (the fourth was never attempted).
- assertEquals(List.of(req1, req2, req4, req6, req7, req8), tester.curator().readNameServiceQueue().requests());
-
-
- // Finally, queue again initially contains a subsequence of requests, 3–6.
- // While the dispatcher is working through those, the queue is altered in unexpected ways—specifically, owner2's requests is gone.
- // The dispatch of requests owned by owner1 still fail, so the original sublist read by the dispatcher
- // is not removed, nor is it replaced by a subsequence, but the processed requests are attempted removed from the current queue.
- tester.curator().writeNameServiceQueue(new NameServiceQueue(base));
- expectations.clear();
- expectations.add(name -> {
- assertEquals(rec1.name(), name);
- tester.curator().writeNameServiceQueue(new NameServiceQueue(List.of(req1, req2, req3, req4, req6, req7, req8)));
- });
- expectations.add(failOwner1.get()); // Recursively adds itself to the tail of the expectation queue.
- expectations.add(name -> assertEquals(rec3.name(), name));
- assertEquals(0.5, dispatcher.maintain()); // 2 of 4 requests were ok (the fourth was never attempted).
- assertEquals(List.of(req2, req3, req4, req6, req7, req8), tester.curator().readNameServiceQueue().requests());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeSchedulerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeSchedulerTest.java
deleted file mode 100644
index 0adb71c1b68..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgradeSchedulerTest.java
+++ /dev/null
@@ -1,254 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.OsRelease;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.maintenance.OsUpgradeScheduler.CalendarVersionedRelease.CalendarVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.ZoneOffset;
-import java.time.format.DateTimeFormatter;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-public class OsUpgradeSchedulerTest {
-
- @Test
- void schedule_calendar_versioned_release() {
- ControllerTester tester = new ControllerTester();
- OsUpgradeScheduler scheduler = new OsUpgradeScheduler(tester.controller(), Duration.ofDays(1));
- Instant t0 = Instant.parse("2022-01-16T09:05:00.00Z"); // Inside trigger period
- tester.clock().setInstant(t0);
-
- CloudName cloud = CloudName.from("cloud");
- ZoneApi zone = zone("prod.us-west-1", cloud);
- tester.zoneRegistry().setZones(zone).dynamicProvisioningIn(zone);
-
- // Initial run does nothing as the cloud does not have a target
- scheduler.maintain();
- assertTrue(tester.controller().os().target(cloud).isEmpty(), "No target set");
-
- // Target is set manually
- Version version0 = Version.fromString("7.0.0.20220101");
- tester.controller().os().upgradeTo(version0, cloud, false, false);
-
- // Target remains unchanged as it hasn't expired yet
- for (var interval : List.of(Duration.ZERO, Duration.ofDays(30))) {
- tester.clock().advance(interval);
- scheduler.maintain();
- assertEquals(version0, tester.controller().os().target(cloud).get().osVersion().version());
- }
-
- // New release becomes available, but is not triggered until cool-down period has passed, and we're inside a
- // trigger period
- Version version1 = Version.fromString("7.0.0.20220301");
- tester.clock().advance(Duration.ofDays(13).plusHours(15));
- assertEquals("2022-03-01T00:05:00", formatInstant(tester.clock().instant()));
-
- // Change does not become available until certification
- Optional<OsUpgradeScheduler.Change> change = scheduler.changeIn(cloud, tester.clock().instant(), true);
- assertTrue(change.isPresent());
- assertFalse(change.get().certified());
- Version systemVersion = tester.controller().readSystemVersion();
- Version olderThanSystemVersion = new Version(systemVersion.getMajor(), systemVersion.getMinor() - 1, systemVersion.getMicro());
- tester.controller().os().certify(version1, cloud, olderThanSystemVersion);
-
- // Change is now certified
- change = scheduler.changeIn(cloud, tester.clock().instant(), true);
- assertTrue(change.isPresent() && change.get().certified());
- assertEquals(version1, change.get().osVersion().version());
- scheduler.maintain();
- assertEquals(version0,
- tester.controller().os().target(cloud).get().osVersion().version(),
- "Target is unchanged because cooldown hasn't passed");
- tester.clock().advance(Duration.ofDays(3).plusHours(18));
- assertEquals("2022-03-04T18:05:00", formatInstant(tester.clock().instant()));
- scheduler.maintain();
- assertEquals(version0,
- tester.controller().os().target(cloud).get().osVersion().version(),
- "Target is unchanged because we're outside trigger period");
- tester.clock().advance(Duration.ofDays(2).plusHours(14));
- assertEquals("2022-03-07T08:05:00", formatInstant(tester.clock().instant()));
-
- // Time constraints have now passed, but the current target has been pinned in the meantime
- tester.controller().os().upgradeTo(version0, cloud, false, true);
- change = scheduler.changeIn(cloud, tester.clock().instant(), true);
- assertTrue(change.isPresent());
- assertTrue(change.get().certified());
- assertEquals(-1, scheduler.maintain());
- assertEquals(version0,
- tester.controller().os().target(cloud).get().osVersion().version(),
- "Target is unchanged because it's pinned");
-
- // Target is unpinned and new version is allowed to be scheduled
- tester.controller().os().upgradeTo(version0, cloud, false, false);
- scheduler.maintain();
- assertEquals(version1,
- tester.controller().os().target(cloud).get().osVersion().version(),
- "New target set");
-
- // A few more days pass and target remains unchanged
- tester.clock().advance(Duration.ofDays(2));
- scheduler.maintain();
- assertEquals(version1, tester.controller().os().target(cloud).get().osVersion().version());
-
- // Estimate next change
- Version expected = Version.fromString("7.0.0.20220426");
- tester.controller().os().certify(expected, cloud, systemVersion);
- Optional<OsUpgradeScheduler.Change> nextChange = scheduler.changeIn(cloud, tester.clock().instant(), true);
- assertTrue(nextChange.isPresent());
- assertEquals(expected, nextChange.get().osVersion().version());
- assertEquals("2022-04-26T07:00:00", formatInstant(nextChange.get().scheduleAt()));
- }
-
- @Test
- void schedule_calendar_versioned_release_in_cd() {
- ControllerTester tester = new ControllerTester(SystemName.cd);
- OsUpgradeScheduler scheduler = new OsUpgradeScheduler(tester.controller(), Duration.ofDays(1));
- Instant t0 = Instant.parse("2022-01-16T02:05:00.00Z"); // Inside trigger period
- tester.clock().setInstant(t0);
- CloudName cloud = CloudName.from("cloud");
- ZoneApi zone = zone("prod.us-west-1", cloud);
- tester.zoneRegistry().setZones(zone).dynamicProvisioningIn(zone);
-
- // Set initial target
- Version version0 = Version.fromString("7.0.0.20220101");
- tester.controller().os().upgradeTo(version0, cloud, false, false);
-
- // Next version is triggered
- Version version1 = Version.fromString("7.0.0.20220301");
- tester.clock().advance(Duration.ofDays(44));
- assertEquals("2022-03-01T02:05:00", formatInstant(tester.clock().instant()));
- scheduler.maintain();
- assertEquals(version0, tester.controller().os().target(cloud).get().osVersion().version());
- // Cool-down passes
- tester.clock().advance(Duration.ofHours(4));
- assertEquals(version1, scheduler.changeIn(cloud, tester.clock().instant(), false).get().osVersion().version());
- scheduler.maintain();
- assertEquals(version1, tester.controller().os().target(cloud).get().osVersion().version());
-
- // Estimate next change
- Optional<OsUpgradeScheduler.Change> nextChange = scheduler.changeIn(cloud, tester.clock().instant(), true);
- assertTrue(nextChange.isPresent());
- assertEquals("7.0.0.20220426", nextChange.get().osVersion().version().toFullString());
- assertEquals("2022-04-26T06:00:00", formatInstant(nextChange.get().scheduleAt()));
- }
-
- @Test
- void schedule_latest_release() {
- ControllerTester tester = new ControllerTester();
- OsUpgradeScheduler scheduler = new OsUpgradeScheduler(tester.controller(), Duration.ofDays(1));
- Instant t0 = Instant.parse("2021-06-22T00:42:12.00Z"); // Outside trigger period
- tester.clock().setInstant(t0);
-
- // Set initial target
- CloudName cloud = tester.controller().clouds().iterator().next();
- Version version0 = Version.fromString("8.0");
- tester.controller().os().upgradeTo(version0, cloud, false, false);
-
- // Stable release (tagged outside trigger period) is scheduled once trigger period opens
- Version version1 = Version.fromString("8.1");
- tester.serviceRegistry().artifactRepository().addRelease(new OsRelease(version1, OsRelease.Tag.latest,
- Instant.parse("2021-06-21T23:59:00.00Z")));
- scheduleUpgradeAfter(Duration.ZERO, version0, scheduler, tester);
-
- // No change yet because it hasn't been certified
- Optional<OsUpgradeScheduler.Change> nextChange = scheduler.changeIn(cloud, tester.clock().instant(), true);
- assertFalse(nextChange.get().certified(), "No change");
-
- // Change is certified and upgrade is scheduled
- Version systemVersion = tester.controller().readSystemVersion();
- tester.controller().os().certify(version1, cloud, systemVersion);
- nextChange = scheduler.changeIn(cloud, tester.clock().instant(), true);
- assertTrue(nextChange.isPresent());
- assertEquals(version1, nextChange.get().osVersion().version());
- assertEquals("2021-06-22T07:00:00", formatInstant(nextChange.get().scheduleAt()));
- scheduleUpgradeAfter(Duration.ofHours(7), version1, scheduler, tester); // Inside trigger period
-
- // A newer version is triggered manually
- Version version3 = Version.fromString("8.3");
- tester.controller().os().upgradeTo(version3, cloud, false, false);
-
- // Nothing happens in next iteration as tagged release is older than manually triggered version
- scheduleUpgradeAfter(Duration.ofDays(7), version3, scheduler, tester);
- assertTrue(scheduler.changeIn(cloud, tester.clock().instant(), true).isEmpty());
- }
-
- @Test
- void schedule_latest_release_in_cd() {
- ControllerTester tester = new ControllerTester(SystemName.cd);
- OsUpgradeScheduler scheduler = new OsUpgradeScheduler(tester.controller(), Duration.ofDays(1));
- Instant t0 = Instant.parse("2021-06-21T07:05:00.00Z"); // Inside trigger period
- tester.clock().setInstant(t0);
-
- // Set initial target
- CloudName cloud = tester.controller().clouds().iterator().next();
- Version version0 = Version.fromString("8.0");
- tester.controller().os().upgradeTo(version0, cloud, false, false);
-
- // Latest release is not scheduled immediately
- Version version1 = Version.fromString("8.1");
- tester.serviceRegistry().artifactRepository().addRelease(new OsRelease(version1, OsRelease.Tag.latest,
- tester.clock().instant()));
- assertEquals(version1, scheduler.changeIn(cloud, tester.clock().instant(), true).get().osVersion().version());
- assertEquals("2021-06-22T07:05:00", formatInstant(scheduler.changeIn(cloud, tester.clock().instant(), true).get().scheduleAt()),
- "Not valid until cool-down period passes");
- scheduleUpgradeAfter(Duration.ZERO, version0, scheduler, tester);
-
- // Cooldown period passes and latest release is scheduled
- scheduleUpgradeAfter(Duration.ofDays(1).plusMinutes(3), version1, scheduler, tester);
- }
-
- @Test
- void schedule_of_calender_versioned_releases() {
- Map<String, String> tests = Map.of("2022-01-01", "2021-12-28",
- "2022-03-01", "2021-12-28",
- "2022-03-02", "2022-03-01",
- "2022-04-30", "2022-03-01",
- "2022-05-01", "2022-04-26",
- "2022-06-30", "2022-06-28",
- "2022-07-01", "2022-06-28",
- "2022-08-28", "2022-06-28",
- "2022-08-29", "2022-08-23");
- tests.forEach((now, expectedVersionDate) -> {
- Instant instant = LocalDate.parse(now).atStartOfDay().toInstant(ZoneOffset.UTC);
- CalendarVersion version = OsUpgradeScheduler.CalendarVersionedRelease.findVersion(instant, Version.fromString("1.0"));
- assertEquals(LocalDate.parse(expectedVersionDate), version.date(), "version to schedule at " + now);
- });
- }
-
- private void scheduleUpgradeAfter(Duration duration, Version version, OsUpgradeScheduler scheduler, ControllerTester tester) {
- tester.clock().advance(duration);
- scheduler.maintain();
- CloudName cloud = tester.controller().clouds().iterator().next();
- OsVersionTarget target = tester.controller().os().target(cloud).get();
- assertEquals(version, target.osVersion().version());
- }
-
- private static ZoneApi zone(String id, CloudName cloud) {
- return ZoneApiMock.newBuilder().withId(id).with(cloud).build();
- }
-
- private static String formatInstant(Instant instant) {
- return LocalDateTime.ofInstant(instant, ZoneOffset.UTC).format(DateTimeFormatter.ISO_DATE_TIME);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgraderTest.java
deleted file mode 100644
index 7e2b99b83f4..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsUpgraderTest.java
+++ /dev/null
@@ -1,392 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.zone.NodeSlice;
-import com.yahoo.config.provision.zone.UpgradePolicy;
-import com.yahoo.config.provision.zone.UpgradePolicy.Step;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.function.UnaryOperator;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-public class OsUpgraderTest {
-
- private final ControllerTester tester = new ControllerTester();
- private final OsVersionStatusUpdater statusUpdater = new OsVersionStatusUpdater(tester.controller(), Duration.ofDays(1));
-
- @Test
- void upgrade_os() {
- CloudName cloud1 = CloudName.from("c1");
- CloudName cloud2 = CloudName.from("c2");
- ZoneApi zone0 = zone("prod.us-north-42", "prod.controller", cloud1);
- ZoneApi zone1 = zone("prod.eu-west-1", cloud1);
- ZoneApi zone2 = zone("prod.us-west-1", cloud1);
- ZoneApi zone3 = zone("prod.us-central-1", cloud1);
- ZoneApi zone4 = zone("prod.us-east-3", cloud1);
- ZoneApi zone5 = zone("prod.us-north-1", cloud2);
- UpgradePolicy upgradePolicy = UpgradePolicy.builder()
- .upgrade(zone0)
- .upgrade(zone1)
- .upgrade(Step.of(zone2, zone3).require(NodeSlice.minCount(1)))
- .upgrade(zone5) // Belongs to a different cloud and is ignored by this upgrader
- .upgrade(zone4)
- .build();
- OsUpgrader osUpgrader = osUpgrader(upgradePolicy, cloud1, false);
-
- // Bootstrap system
- List<ZoneId> nonControllerZones = Stream.of(zone1, zone2, zone3, zone4, zone5)
- .map(ZoneApi::getVirtualId)
- .toList();
- tester.configServer().bootstrap(nonControllerZones, List.of(SystemApplication.tenantHost));
- tester.configServer().addNodes(List.of(zone0.getVirtualId()), List.of(SystemApplication.controllerHost));
-
- // Add system application that exists in a real system, but isn't eligible for OS upgrades
- tester.configServer().addNodes(nonControllerZones, List.of(SystemApplication.configServer));
-
- // Change state of a few nodes. These should not affect convergence
- failNodeIn(zone1, SystemApplication.tenantHost);
- failNodeIn(zone3, SystemApplication.tenantHost);
- Node nodeDeferringOsUpgrade = deferOsUpgradeIn(zone2, SystemApplication.tenantHost);
-
- // New OS version released
- Version version1 = Version.fromString("7.1");
- tester.controller().os().upgradeTo(Version.fromString("7.0"), cloud1, false, false);
- tester.controller().os().upgradeTo(version1, cloud1, false, false);
- assertEquals(1, tester.controller().os().targets().size()); // Only allows one version per cloud
- statusUpdater.maintain();
-
- // zone 0: controllers upgrade first
- osUpgrader.maintain();
- assertWanted(version1, SystemApplication.controllerHost, zone0);
- completeUpgrade(version1, SystemApplication.controllerHost, zone0);
- statusUpdater.maintain();
- assertEquals(3, nodesOn(version1).size());
-
- // zone 1: begins upgrading
- assertWanted(Version.emptyVersion, SystemApplication.tenantHost, zone1);
- osUpgrader.maintain();
- assertWanted(version1, SystemApplication.tenantHost, zone1);
-
- // Other zones remain on previous version (none)
- assertWanted(Version.emptyVersion, SystemApplication.proxy, zone2, zone3, zone4);
-
- // zone 1: completes upgrade
- completeUpgrade(version1, SystemApplication.tenantHost, zone1);
- statusUpdater.maintain();
- assertEquals(5, nodesOn(version1).size());
- assertEquals(11, nodesOn(Version.emptyVersion).size());
-
- // zone 2 and 3: begins upgrading
- osUpgrader.maintain();
- assertWanted(version1, SystemApplication.tenantHost, zone2, zone3);
-
- // zone 4: still on previous version
- assertWanted(Version.emptyVersion, SystemApplication.tenantHost, zone4);
-
- // zone 2 and 3: enough nodes upgrade to satisfy node slice of this step
- completeUpgrade(1, version1, SystemApplication.tenantHost, zone2);
- completeUpgrade(1, version1, SystemApplication.tenantHost, zone3);
- assertEquals(Version.emptyVersion,
- nodeRepository().list(zone2.getVirtualId(), NodeFilter.all().hostnames(nodeDeferringOsUpgrade.hostname()))
- .get(0)
- .currentOsVersion(),
- "Current version is unchanged for node deferring OS upgrade");
-
- // zone 4: begins upgrading
- osUpgrader.maintain();
- assertWanted(version1, SystemApplication.tenantHost, zone4);
-
- // zone 4: completes upgrade
- completeUpgrade(version1, SystemApplication.tenantHost, zone4);
-
- // zone 2 and 3: stragglers complete upgrade
- completeUpgrade(version1, SystemApplication.tenantHost, zone2, zone3);
-
- // Next run does nothing as all zones are upgraded
- osUpgrader.maintain();
- assertWanted(version1, SystemApplication.tenantHost, zone1, zone2, zone3, zone4);
- statusUpdater.maintain();
- assertTrue(tester.controller().os().status().nodesIn(cloud1).stream()
- .filter(node -> !node.hostname().equals(nodeDeferringOsUpgrade.hostname()))
- .allMatch(node -> node.currentVersion().equals(version1)),
- "All non-deferring nodes are on target version");
- }
-
- @Test
- void upgrade_os_nodes_choose_newer_version() {
- CloudName cloud = CloudName.from("cloud");
- ZoneApi zone1 = zone("dev.us-east-1", cloud);
- ZoneApi zone2 = zone("prod.us-west-1", cloud);
- UpgradePolicy upgradePolicy = UpgradePolicy.builder()
- .upgrade(zone1)
- .upgrade(zone2)
- .build();
- OsUpgrader osUpgrader = osUpgrader(upgradePolicy, cloud, false);
-
- // Bootstrap system
- tester.configServer().bootstrap(List.of(zone1.getId(), zone2.getId()),
- List.of(SystemApplication.tenantHost));
-
- // New OS version released
- Version version = Version.fromString("7.1");
- tester.controller().os().upgradeTo(Version.fromString("7.0"), cloud, false, false);
- tester.controller().os().upgradeTo(version, cloud, false, false); // Replaces existing target
- statusUpdater.maintain();
-
- // zone 1 upgrades
- osUpgrader.maintain();
- assertWanted(version, SystemApplication.tenantHost, zone1);
- Version chosenVersion = Version.fromString("7.1.1"); // Upgrade mechanism chooses a slightly newer version
- completeUpgrade(Integer.MAX_VALUE, version, chosenVersion, SystemApplication.tenantHost, zone1);
- statusUpdater.maintain();
- assertEquals(3, nodesOn(chosenVersion).size());
-
- // zone 2 upgrades
- osUpgrader.maintain();
- assertWanted(version, SystemApplication.tenantHost, zone2);
- completeUpgrade(Integer.MAX_VALUE, version, chosenVersion, SystemApplication.tenantHost, zone2);
- statusUpdater.maintain();
- assertEquals(6, nodesOn(chosenVersion).size());
-
- // No more upgrades
- osUpgrader.maintain();
- assertWanted(version, SystemApplication.tenantHost, zone1, zone2);
- assertTrue(tester.controller().os().status().nodesIn(cloud).stream()
- .noneMatch(node -> node.currentVersion().isBefore(version)), "All nodes on target version or newer");
- }
-
- @Test
- public void downgrade_os() {
- CloudName cloud = CloudName.from("cloud");
- ZoneApi zone1 = zone("dev.us-east-1", cloud);
- ZoneApi zone2 = zone("prod.us-west-1", cloud);
- UpgradePolicy upgradePolicy = UpgradePolicy.builder()
- .upgrade(zone1)
- .upgrade(zone2)
- .build();
- OsUpgrader osUpgrader = osUpgrader(upgradePolicy, cloud, true);
-
- // Bootstrap system
- tester.configServer().bootstrap(List.of(zone1.getId(), zone2.getId()),
- List.of(SystemApplication.tenantHost));
-
- // New OS version released
- Version version0 = Version.fromString("1.0");
- Version version1 = Version.fromString("2.0");
- tester.controller().os().upgradeTo(version1, cloud, false, false);
- statusUpdater.maintain();
-
- // All zones upgrade
- List<ZoneApi> zones = new ArrayList<>(List.of(zone1, zone2));
- for (var zone : zones) {
- osUpgrader.maintain();
- completeUpgrade(version1, SystemApplication.tenantHost, zone);
- statusUpdater.maintain();
- }
- assertTrue(tester.controller().os().status().nodesIn(cloud).stream()
- .allMatch(node -> node.currentVersion().equals(version1)), "All nodes on target version");
-
- // Downgrade is triggered
- tester.controller().os().upgradeTo(version0, cloud, true, false);
- // Zone order is reversed
- Collections.reverse(zones);
-
- // One host in first zone downgrades. Wanted version is not changed for second zone yet
- osUpgrader.maintain();
- completeUpgrade(1, version0, SystemApplication.tenantHost, zones.get(0));
- osUpgrader.maintain();
- assertWanted(version1, SystemApplication.tenantHost, zones.get(1));
-
- // All zones downgrade
- for (var zone : zones) {
- osUpgrader.maintain();
- completeUpgrade(version0, SystemApplication.tenantHost, zone);
- statusUpdater.maintain();
- }
- assertTrue(tester.controller().os().status().nodesIn(cloud).stream()
- .allMatch(node -> node.currentVersion().equals(version0)), "All nodes on target version");
- }
-
- @Test
- public void downgrade_os_partially() {
- CloudName cloud = CloudName.from("cloud");
- ZoneApi zone1 = zone("dev.us-east-1", cloud);
- ZoneApi zone2 = zone("prod.us-west-1", cloud);
- UpgradePolicy upgradePolicy = UpgradePolicy.builder()
- .upgrade(zone1)
- .upgrade(zone2)
- .build();
- OsUpgrader osUpgrader = osUpgrader(upgradePolicy, cloud, false);
-
- // Bootstrap system
- tester.configServer().bootstrap(List.of(zone1.getId(), zone2.getId()),
- List.of(SystemApplication.tenantHost));
-
- // New OS version released
- Version version0 = Version.fromString("1.0");
- Version version1 = Version.fromString("2.0");
- tester.controller().os().upgradeTo(version1, cloud, false, false);
- statusUpdater.maintain();
-
- // All zones upgrade
- for (var zone : List.of(zone1, zone2)) {
- osUpgrader.maintain();
- completeUpgrade(version1, SystemApplication.tenantHost, zone);
- statusUpdater.maintain();
- }
- assertTrue(tester.controller().os().status().nodesIn(cloud).stream()
- .allMatch(node -> node.currentVersion().equals(version1)), "All nodes on target version");
-
- // Downgrade is triggered
- tester.controller().os().upgradeTo(version0, cloud, true, false);
-
- // All zones downgrade, in reverse order
- for (var zone : List.of(zone2, zone1)) {
- osUpgrader.maintain();
- // Partial downgrading happens, as this decision is left up to the zone. Downgrade target is still set in
- // all zones as a best-effort, and to halt any further upgrades
- completeUpgrade(1, version0, SystemApplication.tenantHost, zone);
- statusUpdater.maintain();
- }
- int zoneCount = 2;
- Map<Version, Long> currentVersions = tester.controller().os().status().nodesIn(cloud).stream()
- .collect(Collectors.groupingBy(NodeVersion::currentVersion,
- Collectors.counting()));
- assertEquals(1 * zoneCount, currentVersions.get(version0));
- assertEquals(2 * zoneCount, currentVersions.get(version1));
- }
-
- private List<NodeVersion> nodesOn(Version version) {
- return tester.controller().os().status().versions().entrySet().stream()
- .filter(entry -> entry.getKey().version().equals(version))
- .flatMap(entry -> entry.getValue().stream())
- .toList();
- }
-
- private void assertCurrent(Version version, SystemApplication application, ZoneApi... zones) {
- assertVersion(application, version, Node::currentOsVersion, zones);
- }
-
- private void assertWanted(Version version, SystemApplication application, ZoneApi... zones) {
- for (var zone : zones) {
- assertEquals(version,
- nodeRepository().targetVersionsOf(zone.getVirtualId()).osVersion(application.nodeType())
- .orElse(Version.emptyVersion),
- "Target version set for " + application + " in " + zone.getVirtualId());
- }
- }
-
- private void assertVersion(SystemApplication application, Version version, Function<Node, Version> versionField,
- ZoneApi... zones) {
- for (ZoneApi zone : zones) {
- for (Node node : nodesRequiredToUpgrade(zone, application)) {
- assertEquals(version, versionField.apply(node), application + " version in " + zone.getId());
- }
- }
- }
-
- private List<Node> nodesRequiredToUpgrade(ZoneApi zone, SystemApplication application) {
- return nodeRepository().list(zone.getVirtualId(), NodeFilter.all().applications(application.id()))
- .stream()
- .filter(node -> OsUpgrader.canUpgrade(node, false))
- .toList();
- }
-
- private Node failNodeIn(ZoneApi zone, SystemApplication application) {
- return patchOneNodeIn(zone, application, (node) -> Node.builder(node).state(Node.State.failed).build());
- }
-
- private Node deferOsUpgradeIn(ZoneApi zone, SystemApplication application) {
- return patchOneNodeIn(zone, application, (node) -> Node.builder(node).deferOsUpgrade(true).build());
- }
-
- private Node patchOneNodeIn(ZoneApi zone, SystemApplication application, UnaryOperator<Node> patcher) {
- List<Node> nodes = nodeRepository().list(zone.getVirtualId(), NodeFilter.all().applications(application.id()));
- if (nodes.isEmpty()) {
- throw new IllegalArgumentException("No nodes allocated to " + application.id());
- }
- Node node = nodes.get(0);
- Node newNode = patcher.apply(node);
- nodeRepository().putNodes(zone.getVirtualId(), newNode);
- return newNode;
- }
-
- /** Simulate OS upgrade of nodes allocated to application. In a real system this is done by the node itself */
- private void completeUpgrade(Version version, SystemApplication application, ZoneApi... zones) {
- completeUpgrade(-1, version, application, zones);
- }
-
- private void completeUpgrade(int nodeCount, Version version, SystemApplication application, ZoneApi... zones) {
- completeUpgrade(nodeCount, version, version, application, zones);
- }
-
- private void completeUpgrade(int nodeCount, Version wantedVersion, Version currentVersion, SystemApplication application, ZoneApi... zones) {
- assertWanted(wantedVersion, application, zones);
- for (ZoneApi zone : zones) {
- int nodesUpgraded = 0;
- List<Node> nodes = nodesRequiredToUpgrade(zone, application);
- for (Node node : nodes) {
- if (node.currentVersion().equals(wantedVersion)) continue;
- nodeRepository().putNodes(zone.getVirtualId(), Node.builder(node)
- .wantedOsVersion(currentVersion)
- .currentOsVersion(currentVersion)
- .build());
- if (++nodesUpgraded == nodeCount) {
- break;
- }
- }
- if (nodesUpgraded == nodes.size()) {
- assertCurrent(currentVersion, application, zone);
- }
- }
- }
-
- private NodeRepositoryMock nodeRepository() {
- return tester.configServer().nodeRepository();
- }
-
- private OsUpgrader osUpgrader(UpgradePolicy upgradePolicy, CloudName cloud, boolean dynamicProvisioning) {
- var zones = upgradePolicy.steps().stream().map(Step::zones).flatMap(Collection::stream).toList();
- tester.zoneRegistry()
- .setZones(zones)
- .setOsUpgradePolicy(cloud, upgradePolicy);
- if (dynamicProvisioning) {
- tester.zoneRegistry().dynamicProvisioningIn(zones);
- }
- return new OsUpgrader(tester.controller(), Duration.ofDays(1), cloud);
- }
-
- private static ZoneApi zone(String id, CloudName cloud) {
- return ZoneApiMock.newBuilder().withId(id).with(cloud).build();
- }
-
- private static ZoneApi zone(String id, String virtualId, CloudName cloud) {
- return ZoneApiMock.newBuilder().withId(id).withVirtualId(virtualId).with(cloud).build();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdaterTest.java
deleted file mode 100644
index bfe140aa4c1..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OsVersionStatusUpdaterTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.zone.UpgradePolicy;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.versions.CertifiedOsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-public class OsVersionStatusUpdaterTest {
-
- @Test
- void test_update() {
- ControllerTester tester = new ControllerTester();
- OsVersionStatusUpdater statusUpdater = new OsVersionStatusUpdater(tester.controller(), Duration.ofDays(1));
- // Add all zones to upgrade policy
- UpgradePolicy.Builder upgradePolicy = UpgradePolicy.builder();
- for (ZoneApi zone : tester.zoneRegistry().zones().controllerUpgraded().zones()) {
- upgradePolicy = upgradePolicy.upgrade(zone);
- }
- tester.zoneRegistry().setOsUpgradePolicy(CloudName.DEFAULT, upgradePolicy.build());
-
- // Initially empty
- assertSame(OsVersionStatus.empty, tester.controller().os().status());
-
- // Setting a new target adds it to current status
- Version version1 = Version.fromString("7.1");
- CloudName cloud0 = CloudName.DEFAULT;
- tester.controller().os().upgradeTo(version1, cloud0, false, false);
- statusUpdater.maintain();
-
- var osVersions = tester.controller().os().status().versions();
- assertEquals(3, osVersions.size());
- assertFalse(osVersions.get(new OsVersion(Version.emptyVersion, cloud0)).isEmpty(), "All nodes on unknown version");
- assertTrue(osVersions.get(new OsVersion(version1, cloud0)).isEmpty(), "No nodes on current target");
-
- CloudName cloud1 = CloudName.AWS;
- Version version2 = Version.fromString("7.0");
- tester.controller().os().upgradeTo(version2, cloud1, false, false);
- statusUpdater.maintain();
-
- osVersions = tester.controller().os().status().versions();
- assertEquals(4, osVersions.size()); // 2 in cloud, 2 in otherCloud.
- assertFalse(osVersions.get(new OsVersion(Version.emptyVersion, cloud0)).isEmpty(), "All nodes on unknown version");
- assertTrue(osVersions.get(new OsVersion(version1, cloud0)).isEmpty(), "No nodes on current target");
- assertFalse(osVersions.get(new OsVersion(Version.emptyVersion, cloud1)).isEmpty(), "All nodes on unknown version");
- assertTrue(osVersions.get(new OsVersion(version2, cloud1)).isEmpty(), "No nodes on current target");
-
- // Updating status cleans up stale certifications
- Set<OsVersion> knownVersions = osVersions.keySet().stream()
- .filter(osVersion -> !osVersion.version().isEmpty())
- .collect(Collectors.toSet());
- // Known versions
- for (OsVersion version : knownVersions) {
- tester.controller().os().certify(version.version(), version.cloud(), Version.fromString("1.2.3"));
- }
- // Additional versions
- OsVersion staleVersion0 = new OsVersion(Version.fromString("7.0"), cloud0); // Only removed for this cloud
- OsVersion staleVersion1 = new OsVersion(Version.fromString("3.11"), cloud0); // Stale in both clouds
- OsVersion staleVersion2 = new OsVersion(Version.fromString("3.11"), cloud1);
- OsVersion futureVersion = new OsVersion(Version.fromString("98.0.2"), cloud0); // Keep future version
- for (OsVersion version : List.of(staleVersion0, staleVersion1, staleVersion2, futureVersion)) {
- tester.controller().os().certify(version.version(), version.cloud(), Version.fromString("1.2.3"));
- }
- statusUpdater.maintain();
- assertEquals(Stream.concat(knownVersions.stream(), Stream.of(futureVersion)).sorted().toList(),
- certifiedOsVersions(tester));
- }
-
- private static List<OsVersion> certifiedOsVersions(ControllerTester tester) {
- return tester.controller().os().readCertified().stream()
- .map(CertifiedOsVersion::osVersion)
- .sorted()
- .toList();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java
deleted file mode 100644
index 09f3d7453db..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/OutstandingChangeDeployerTest.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import org.junit.jupiter.api.Test;
-
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author bratseth
- */
-public class OutstandingChangeDeployerTest {
-
- @Test
- void testChangeDeployer() {
- DeploymentTester tester = new DeploymentTester();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .revisionChange("when-failing")
- .build();
-
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- Version version = new Version(6, 2);
- tester.deploymentTrigger().forceChange(app.instanceId(), Change.of(version));
- assertEquals(Change.of(version), app.instance().change());
- assertFalse(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets());
-
- app.submit(applicationPackage);
- Optional<RevisionId> revision = app.lastSubmission();
- assertFalse(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets());
- assertEquals(Change.of(version).with(revision.get()), app.instance().change());
-
- app.submit(applicationPackage);
- Optional<RevisionId> outstanding = app.lastSubmission();
- assertTrue(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets());
- assertEquals(Change.of(version).with(revision.get()), app.instance().change());
-
- tester.outstandingChangeDeployer().run();
- assertTrue(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets());
- assertEquals(Change.of(version).with(revision.get()), app.instance().change());
-
- app.deploy();
- tester.outstandingChangeDeployer().run();
- assertFalse(app.deploymentStatus().outstandingChange(app.instance().name()).hasTargets());
- assertEquals(Change.of(outstanding.get()), app.instance().change());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ReindexingTriggererTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ReindexingTriggererTest.java
deleted file mode 100644
index 478c0a10585..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ReindexingTriggererTest.java
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing.Cluster;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing.Status;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.ZonedDateTime;
-import java.util.List;
-import java.util.Map;
-
-import static com.yahoo.vespa.hosted.controller.maintenance.ReindexingTriggerer.inWindowOfOpportunity;
-import static com.yahoo.vespa.hosted.controller.maintenance.ReindexingTriggerer.reindexingIsReady;
-import static com.yahoo.vespa.hosted.controller.maintenance.ReindexingTriggerer.reindexingPeriod;
-import static java.time.DayOfWeek.FRIDAY;
-import static java.time.DayOfWeek.MONDAY;
-import static java.time.DayOfWeek.THURSDAY;
-import static java.time.DayOfWeek.TUESDAY;
-import static java.time.DayOfWeek.WEDNESDAY;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class ReindexingTriggererTest {
-
- @Test
- void testWindowOfOpportunity() {
- Duration interval = Duration.ofHours(1);
- Instant now = Instant.now();
- Instant doom = now.plus(ReindexingTriggerer.reindexingPeriod);
- int triggered = 0;
- while (now.isBefore(doom)) {
- if (inWindowOfOpportunity(now, ApplicationId.defaultId(), ZoneId.defaultId())) {
- triggered++;
- ZonedDateTime time = ZonedDateTime.ofInstant(now, java.time.ZoneId.of("Europe/Oslo"));
- assertTrue(List.of(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY).contains(time.getDayOfWeek()));
- assertTrue(List.of(8, 9, 10, 11).contains(time.getHour()));
- }
- now = now.plus(interval);
- }
- // Summer/winter time :'(
- assertTrue(3 <= triggered && triggered <= 5, "Should be in window of opportunity three to five times each period");
- }
-
- @Test
- void testReindexingIsReady() {
- Instant then = Instant.now();
- ApplicationReindexing reindexing = new ApplicationReindexing(true,
- Map.of("c", new Cluster(Map.of(), Map.of("d", new Status(then)))));
-
- Instant now = then;
- assertFalse(reindexingIsReady(reindexing, now),
- "Should not be ready less than one half-period after last triggering");
-
- now = now.plus(reindexingPeriod.dividedBy(2));
- assertFalse(reindexingIsReady(reindexing, now),
- "Should not be ready one half-period after last triggering");
-
- now = now.plusMillis(1);
- assertTrue(reindexingIsReady(reindexing, now),
- "Should be ready more than one half-period after last triggering");
-
- reindexing = new ApplicationReindexing(true,
- Map.of("cluster",
- new Cluster(Map.of(),
- Map.of("type",
- new Status(then, then, null, null, null, null, 1.0, null)))));
- assertFalse(reindexingIsReady(reindexing, now),
- "Should not be ready when reindexing is already running");
-
- reindexing = new ApplicationReindexing(true,
- Map.of("cluster",
- new Cluster(Map.of("type", 123L),
- Map.of("type",
- new Status(then, then, now, null, null, null, 1.0, null)))));
- assertTrue(reindexingIsReady(reindexing, now),
- "Should be ready when reindexing is no longer running");
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java
deleted file mode 100644
index d93dcf71317..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceMeterMaintainerTest.java
+++ /dev/null
@@ -1,178 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.Cloud;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.ClusterResources;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanRegistryMock;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceDatabaseClientMock;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceSnapshot;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.integration.MetricsMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.function.BiConsumer;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author olaa
- */
-public class ResourceMeterMaintainerTest {
-
- private final ControllerTester tester = new ControllerTester(SystemName.Public);
- private final ResourceDatabaseClientMock resourceClient = new ResourceDatabaseClientMock(new PlanRegistryMock());
- private final MetricsMock metrics = new MetricsMock();
- private final ResourceMeterMaintainer maintainer =
- new ResourceMeterMaintainer(tester.controller(), Duration.ofMinutes(5), metrics, resourceClient);
-
- @Test
- void updates_deployment_costs() {
- ApplicationId app1 = ApplicationId.from("t1", "a1", "default");
- ApplicationId app2 = ApplicationId.from("t2", "a1", "default");
- ZoneId z1 = ZoneId.from("prod.aws-us-east-1c");
- ZoneId z2 = ZoneId.from("prod.aws-eu-west-1a");
-
- DeploymentTester deploymentTester = new DeploymentTester(tester);
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder().region(z1.region()).region(z2.region()).trustDefaultCertificate().build();
- List.of(app1, app2).forEach(app -> deploymentTester.newDeploymentContext(app).submit(applicationPackage).deploy());
-
- BiConsumer<ApplicationId, Map<ZoneId, Double>> assertCost = (appId, costs) ->
- assertEquals(costs, tester.controller().applications().getInstance(appId).get().deployments().entrySet().stream()
- .filter(entry -> entry.getValue().cost().isPresent())
- .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().cost().getAsDouble())));
-
- List<ResourceSnapshot> resourceSnapshots = List.of(
- new ResourceSnapshot(app1, resources(12, 34, 56), Instant.EPOCH, z1, 0, CloudAccount.empty),
- new ResourceSnapshot(app1, resources(23, 45, 67), Instant.EPOCH, z2, 0, CloudAccount.empty),
- new ResourceSnapshot(app2, resources(34, 56, 78), Instant.EPOCH, z1, 0, CloudAccount.empty));
-
- maintainer.updateDeploymentCost(resourceSnapshots);
- assertCost.accept(app1, Map.of(z1, 1.72, z2, 3.05));
- assertCost.accept(app2, Map.of(z1, 4.39));
-
- // Remove a region from app1 and add region to app2
- resourceSnapshots = List.of(
- new ResourceSnapshot(app1, resources(23, 45, 67), Instant.EPOCH, z2, 0, CloudAccount.empty),
- new ResourceSnapshot(app2, resources(34, 56, 78), Instant.EPOCH, z1, 0, CloudAccount.empty),
- new ResourceSnapshot(app2, resources(45, 67, 89), Instant.EPOCH, z2, 0, CloudAccount.empty));
-
- maintainer.updateDeploymentCost(resourceSnapshots);
- assertCost.accept(app1, Map.of(z2, 3.05));
- assertCost.accept(app2, Map.of(z1, 4.39, z2, 5.72));
- assertEquals(1.72,
- (Double) metrics.getMetric(context ->
- z1.value().equals(context.get("zoneId")) &&
- app1.tenant().value().equals(context.get("tenantName")),
- "metering.cost.hourly").get(),
- Double.MIN_VALUE);
- }
-
- @Test
- void testMaintainer() {
- setUpZones();
- long lastRefreshTime = tester.clock().millis();
- tester.curator().writeMeteringRefreshTime(lastRefreshTime);
- maintainer.maintain();
- Collection<ResourceSnapshot> consumedResources = resourceClient.resourceSnapshots();
-
- // The mocked repository contains two applications, so we should also consume two ResourceSnapshots
- assertEquals(4, consumedResources.size());
- ResourceSnapshot app1 = consumedResources.stream().filter(snapshot -> snapshot.getApplicationId().equals(ApplicationId.from("tenant1", "app1", "default"))).findFirst().orElseThrow();
- ResourceSnapshot app2 = consumedResources.stream().filter(snapshot -> snapshot.getApplicationId().equals(ApplicationId.from("tenant2", "app2", "default"))).findFirst().orElseThrow();
-
- assertEquals(24, app1.resources().vcpu(), Double.MIN_VALUE);
- assertEquals(24, app1.resources().memoryGb(), Double.MIN_VALUE);
- assertEquals(500, app1.resources().diskGb(), Double.MIN_VALUE);
-
- assertEquals(40, app2.resources().vcpu(), Double.MIN_VALUE);
- assertEquals(24, app2.resources().memoryGb(), Double.MIN_VALUE);
- assertEquals(500, app2.resources().diskGb(), Double.MIN_VALUE);
-
- assertEquals(tester.clock().millis() / 1000, metrics.getMetric("metering_last_reported"));
- assertEquals(2224.0d, (Double) metrics.getMetric("metering_total_reported"), Double.MIN_VALUE);
- assertEquals(24d, (Double) metrics.getMetric(context -> "tenant1".equals(context.get("tenantName")), "metering.vcpu").get(), Double.MIN_VALUE);
- assertEquals(40d, (Double) metrics.getMetric(context -> "tenant2".equals(context.get("tenantName")), "metering.vcpu").get(), Double.MIN_VALUE);
-
- // Metering is not refreshed
- assertFalse(resourceClient.hasRefreshedMaterializedView());
- assertEquals(lastRefreshTime, tester.curator().readMeteringRefreshTime());
-
- var millisAdvanced = 3600 * 1000;
- tester.clock().advance(Duration.ofMillis(millisAdvanced));
- maintainer.maintain();
- assertTrue(resourceClient.hasRefreshedMaterializedView());
- assertEquals(lastRefreshTime + millisAdvanced, tester.curator().readMeteringRefreshTime());
- }
-
- @Test
- public void testClusterCost() {
- var nodeResources = new NodeResources(10, 64, 100, 10,
- NodeResources.DiskSpeed.fast,
- NodeResources.StorageType.local,
- NodeResources.Architecture.x86_64,
- new NodeResources.GpuResources(2, 16));
- var clusterResources = new ClusterResources(5, 1, nodeResources);
-
- assertEquals(5 * nodeResources.cost(), ResourceMeterMaintainer.cost(clusterResources, SystemName.Public), 0.001);
- }
-
- private void setUpZones() {
- ZoneApiMock zone1 = ZoneApiMock.newBuilder().withId("prod.region-2").build();
- ZoneApiMock zone2 = ZoneApiMock.newBuilder().withId("test.region-3").build();
- tester.zoneRegistry().setZones(zone1, zone2);
- tester.configServer().nodeRepository().addFixedNodes(zone1.getId());
- tester.configServer().nodeRepository().addFixedNodes(zone2.getId());
- tester.configServer().nodeRepository().putNodes(zone1.getId(), createNodes());
- }
-
- private List<Node> createNodes() {
- return Stream.of(Node.State.provisioned,
- Node.State.ready,
- Node.State.dirty,
- Node.State.failed,
- Node.State.parked,
- Node.State.active)
- .map(state -> Node.builder()
- .hostname(HostName.of("host" + state))
- .parentHostname(HostName.of("parenthost" + state))
- .state(state)
- .type(NodeType.tenant)
- .owner(ApplicationId.from("tenant1", "app1", "default"))
- .currentVersion(Version.fromString("7.42"))
- .wantedVersion(Version.fromString("7.42"))
- .currentOsVersion(Version.fromString("7.6"))
- .wantedOsVersion(Version.fromString("7.6"))
- .serviceState(Node.ServiceState.expectedUp)
- .resources(new NodeResources(24, 24, 500, 1))
- .clusterId("clusterA")
- .clusterType(state == Node.State.active ? Node.ClusterType.admin : Node.ClusterType.container)
- .build())
- .toList();
- }
-
- private NodeResources resources(double cpu, double ram, double disk) {
- return new NodeResources(cpu, ram, disk, 0, NodeResources.DiskSpeed.getDefault(), NodeResources.StorageType.getDefault(), NodeResources.Architecture.getDefault(), NodeResources.GpuResources.getDefault());
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceTagMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceTagMaintainerTest.java
deleted file mode 100644
index f3ca6ba7b41..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/ResourceTagMaintainerTest.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.MockResourceTagger;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author olaa
- */
-public class ResourceTagMaintainerTest {
-
- private final ControllerTester tester = new ControllerTester();
-
- @Test
- void maintain() {
- setUpZones();
- MockResourceTagger mockResourceTagger = new MockResourceTagger();
- ResourceTagMaintainer resourceTagMaintainer = new ResourceTagMaintainer(tester.controller(),
- Duration.ofMinutes(5),
- mockResourceTagger);
- resourceTagMaintainer.maintain();
- assertEquals(2, mockResourceTagger.getValues().size());
- Map<HostName, ApplicationId> applicationForHost = mockResourceTagger.getValues().get(ZoneId.from("prod.region-2"));
- assertEquals(ApplicationId.from("t1", "a1", "i1"), applicationForHost.get(HostName.of("parentHostA.prod.region-2")));
- assertEquals(ApplicationId.from("hosted-vespa", "shared-host", "default"), applicationForHost.get(HostName.of("parentHostB.prod.region-2")));
- assertEquals(ApplicationId.from("hosted-vespa", "shared-host", "admin"), applicationForHost.get(HostName.of("parentHostC.prod.region-2")));
- }
-
- private void setUpZones() {
- ZoneApiMock nonAwsZone = ZoneApiMock.newBuilder().withId("test.region-1").build();
- ZoneApiMock awsZone1 = ZoneApiMock.newBuilder().withId("prod.region-2").withCloud("aws").build();
- ZoneApiMock awsZone2 = ZoneApiMock.newBuilder().withId("test.region-3").withCloud("aws").build();
- tester.zoneRegistry().setZones(nonAwsZone, awsZone1, awsZone2);
- setNodes(awsZone1.getId());
- setNodes(nonAwsZone.getId());
- }
-
- public void setNodes(ZoneId zone) {
- var hostA = Node.builder()
- .hostname(HostName.of("parentHostA." + zone.value()))
- .type(NodeType.host)
- .owner(ApplicationId.from(SystemApplication.TENANT.value(), "tenant-host", "default"))
- .exclusiveTo(ApplicationId.from("t1", "a1", "i1"))
- .build();
- var nodeA = Node.builder()
- .hostname(HostName.of("hostA." + zone.value()))
- .type(NodeType.tenant)
- .parentHostname(HostName.of("parentHostA." + zone.value()))
- .owner(ApplicationId.from("tenant1", "app1", "default"))
- .build();
- var hostB = Node.builder()
- .hostname(HostName.of("parentHostB." + zone.value()))
- .type(NodeType.host)
- .owner(ApplicationId.from(SystemApplication.TENANT.value(), "tenant-host", "default"))
- .build();
- var hostC = Node.builder()
- .hostname(HostName.of("parentHostC." + zone.value()))
- .type(NodeType.host)
- .exclusiveToClusterType(Node.ClusterType.admin)
- .owner(ApplicationId.from(SystemApplication.TENANT.value(), "tenant-host", "default"))
- .build();
- tester.configServer().nodeRepository().putNodes(zone, List.of(hostA, nodeA, hostB, hostC));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RetriggerMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RetriggerMaintainerTest.java
deleted file mode 100644
index 923fa34abf1..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/RetriggerMaintainerTest.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.deployment.RetriggerEntry;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mortent
- */
-public class RetriggerMaintainerTest {
-
- private final DeploymentTester tester = new DeploymentTester();
-
- @Test
- void processes_queue() {
- RetriggerMaintainer maintainer = new RetriggerMaintainer(tester.controller(), Duration.ofDays(1));
- ApplicationId applicationId = ApplicationId.from("tenant", "app", "default");
- var devApp = tester.newDeploymentContext(applicationId);
- ApplicationPackage appPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
-
- // Deploy app
- devApp.runJob(DeploymentContext.devUsEast1, appPackage);
- devApp.completeRollout();
-
- // Trigger a run (to simulate a running job)
- tester.deploymentTrigger().reTrigger(applicationId, DeploymentContext.devUsEast1, null);
-
- // Add a job to the queue
- tester.deploymentTrigger().reTriggerOrAddToQueue(devApp.deploymentIdIn(ZoneId.from("dev", "us-east-1")), null);
-
- // Should be 1 entry in the queue:
- List<RetriggerEntry> retriggerEntries = tester.controller().curator().readRetriggerEntries();
- assertEquals(1, retriggerEntries.size());
-
- // Adding to queue triggers abort
- devApp.jobAborted(DeploymentContext.devUsEast1);
- assertEquals(0, tester.jobs().active(applicationId).size());
-
- // The maintainer runs and will actually trigger dev us-east, but keeps the entry in queue to verify it was actually run
- maintainer.maintain();
- retriggerEntries = tester.controller().curator().readRetriggerEntries();
- assertEquals(1, retriggerEntries.size());
- assertEquals(1, tester.jobs().active(applicationId).size());
-
- // Run outstanding jobs
- devApp.runJob(DeploymentContext.devUsEast1);
- assertEquals(0, tester.jobs().active(applicationId).size());
-
- // Run maintainer again, should find that the job has already run successfully and will remove the entry.
- maintainer.maintain();
- retriggerEntries = tester.controller().curator().readRetriggerEntries();
- assertEquals(0, retriggerEntries.size());
- assertEquals(0, tester.jobs().active(applicationId).size());
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgraderTest.java
deleted file mode 100644
index d38c2006bf5..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/SystemUpgraderTest.java
+++ /dev/null
@@ -1,479 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.zone.UpgradePolicy;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author mpolden
- */
-public class SystemUpgraderTest {
-
- private static final ZoneApi zone1 = ZoneApiMock.fromId("prod.eu-west-1");
- private static final ZoneApi zone2 = ZoneApiMock.fromId("prod.us-west-1");
- private static final ZoneApi zone3 = ZoneApiMock.fromId("prod.us-central-1");
- private static final ZoneApi zone4 = ZoneApiMock.fromId("prod.us-east-3");
-
- private final ControllerTester tester = new ControllerTester();
-
- @Test
- void upgrade_system() {
- SystemUpgrader systemUpgrader = systemUpgrader(
- UpgradePolicy.builder()
- .upgrade(zone1)
- .upgradeInParallel(zone2, zone3)
- .upgrade(zone4)
- .build()
- );
-
- Version version1 = Version.fromString("6.5");
- // Bootstrap a system without host applications
- tester.configServer().bootstrap(List.of(zone1.getId(), zone2.getId(), zone3.getId(), zone4.getId()),
- SystemApplication.configServer, SystemApplication.proxy);
- // Fail a few nodes. Failed nodes should not affect versions
- failNodeIn(zone1, SystemApplication.configServer);
- failNodeIn(zone3, SystemApplication.proxy);
- tester.upgradeSystem(version1);
- systemUpgrader.maintain();
- assertCurrentVersion(SystemApplication.configServer, version1, zone1, zone2, zone3, zone4);
- assertCurrentVersion(SystemApplication.proxy, version1, zone1, zone2, zone3, zone4);
- assertSystemVersion(version1);
-
- // Controller upgrades
- Version version2 = Version.fromString("6.6");
- tester.upgradeController(version2);
- assertControllerVersion(version2);
-
- // System upgrade starts
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.configServer, version2, zone1);
- // Other zones remain on previous version
- assertWantedVersion(SystemApplication.configServer, version1, zone2, zone3, zone4);
- // Proxy application is not upgraded yet
- assertWantedVersion(SystemApplication.proxy, version1, zone1, zone2, zone3, zone4);
-
- // zone1: config server upgrades and proxy application
- completeUpgrade(SystemApplication.configServer, version2, zone1);
- systemUpgrader.maintain();
-
- // zone 1: proxy application upgrades
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.proxy, version2, zone1);
- completeUpgrade(SystemApplication.proxy, version2, zone1);
- assertTrue(tester.configServer().application(SystemApplication.proxy.id(), zone1.getId()).isPresent(),
- "Deployed proxy application");
-
- // zone 2, 3 and 4: still targets old version
- assertWantedVersion(SystemApplication.configServer, version1, zone2, zone3, zone4);
- assertWantedVersion(SystemApplication.proxy, version1, zone2, zone3, zone4);
-
- // zone 2 and 3: upgrade does not start until zone 1 proxy application config converges
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.configServer, version1, zone2, zone3);
- convergeServices(SystemApplication.proxy, zone1);
-
- // zone 2 and 3: config server upgrades, first in zone 2, then in zone 3
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.configServer, version2, zone2, zone3);
- assertWantedVersion(SystemApplication.configServer, version1, zone4);
- assertWantedVersion(SystemApplication.proxy, version1, zone2, zone3, zone4);
- completeUpgrade(SystemApplication.configServer, version2, zone2);
-
- // proxy application starts upgrading in zone 2, while config server completes upgrade in zone 3
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.proxy, version2, zone2);
- assertWantedVersion(SystemApplication.proxy, version1, zone3);
- completeUpgrade(SystemApplication.configServer, version2, zone3);
-
- // zone 2 and 3: proxy application upgrades in parallel
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.proxy, version2, zone2, zone3);
- completeUpgrade(SystemApplication.proxy, version2, zone2, zone3);
- convergeServices(SystemApplication.proxy, zone2, zone3);
-
- // zone 4: config server upgrades
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.configServer, version2, zone4);
- assertWantedVersion(SystemApplication.proxy, version1, zone4);
- // zone 4: proxy application does not upgrade until all config servers are done
- completeUpgrade(2, SystemApplication.configServer, version2, zone4);
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.proxy, version1, zone4);
- completeUpgrade(1, SystemApplication.configServer, version2, zone4);
-
- // System version remains unchanged until final application upgrades
- tester.computeVersionStatus();
- assertSystemVersion(version1);
-
- // zone 4: proxy application upgrades
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.proxy, version2, zone4);
- completeUpgrade(SystemApplication.proxy, version2, zone4);
-
- // zone 4: System version remains unchanged until config converges
- tester.computeVersionStatus();
- assertSystemVersion(version1);
- convergeServices(SystemApplication.proxy, zone4);
- tester.computeVersionStatus();
- assertSystemVersion(version2);
-
- // Next run does nothing as system is now upgraded
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.configServer, version2, zone1, zone2, zone3, zone4);
- assertWantedVersion(SystemApplication.proxy, version2, zone1, zone2, zone3, zone4);
- }
-
- @Test
- void upgrade_controller_with_non_converging_application() {
- SystemUpgrader systemUpgrader = systemUpgrader(UpgradePolicy.builder().upgrade(zone1).build());
-
- // Bootstrap system
- tester.configServer().bootstrap(List.of(zone1.getId()), SystemApplication.configServer,
- SystemApplication.proxy);
- Version version1 = Version.fromString("6.5");
- tester.upgradeSystem(version1);
-
- // Controller upgrades
- Version version2 = Version.fromString("6.6");
- tester.upgradeController(version2);
-
- // zone 1: System applications upgrade
- systemUpgrader.maintain();
- completeUpgrade(SystemApplication.configServer, version2, zone1);
- systemUpgrader.maintain();
- completeUpgrade(SystemApplication.proxy, version2, zone1);
- tester.computeVersionStatus();
- assertSystemVersion(version1); // Unchanged until proxy application converges
-
- // Controller upgrades again
- Version version3 = Version.fromString("6.7");
- tester.upgradeController(version3);
- assertSystemVersion(version1);
- assertControllerVersion(version3);
-
- // zone 1: proxy application converges and system version changes
- convergeServices(SystemApplication.proxy, zone1);
- tester.computeVersionStatus();
- assertSystemVersion(version2);
- assertControllerVersion(version3);
- }
-
- @Test
- void upgrade_system_containing_host_applications() {
- SystemUpgrader systemUpgrader = systemUpgrader(
- UpgradePolicy.builder()
- .upgrade(zone1)
- .upgradeInParallel(zone2, zone3)
- .upgrade(zone4)
- .build()
- );
-
- Version version1 = Version.fromString("6.5");
- tester.configServer().bootstrap(List.of(zone1.getId(), zone2.getId(), zone3.getId(), zone4.getId()), SystemApplication.notController());
- tester.upgradeSystem(version1);
- systemUpgrader.maintain();
- assertCurrentVersion(SystemApplication.notController(), version1, zone1, zone2, zone3, zone4);
-
- // Controller upgrades
- Version version2 = Version.fromString("6.6");
- tester.upgradeController(version2);
- assertControllerVersion(version2);
-
- // System upgrades in zone 1:
- systemUpgrader.maintain();
- List<SystemApplication> allExceptZone = List.of(SystemApplication.configServerHost,
- SystemApplication.configServer,
- SystemApplication.proxyHost,
- SystemApplication.tenantHost);
- completeUpgrade(allExceptZone, version2, zone1);
- systemUpgrader.maintain();
- completeUpgrade(SystemApplication.proxy, version2, zone1);
- convergeServices(SystemApplication.proxy, zone1);
- assertWantedVersion(SystemApplication.notController(), version1, zone2, zone3, zone4);
-
- // zone 2 and 3:
- systemUpgrader.maintain();
- completeUpgrade(allExceptZone, version2, zone2, zone3);
- systemUpgrader.maintain();
- completeUpgrade(SystemApplication.proxy, version2, zone2, zone3);
- convergeServices(SystemApplication.proxy, zone2, zone3);
- assertWantedVersion(SystemApplication.notController(), version1, zone4);
-
- // zone 4:
- systemUpgrader.maintain();
- completeUpgrade(allExceptZone, version2, zone4);
- systemUpgrader.maintain();
- completeUpgrade(SystemApplication.proxy, version2, zone4);
-
- // All done
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.notController(), version2, zone1, zone2, zone3, zone4);
- }
-
- @Test
- void downgrading_controller_never_downgrades_system() {
- SystemUpgrader systemUpgrader = systemUpgrader(UpgradePolicy.builder().upgrade(zone1).build());
-
- Version version = Version.fromString("6.5");
- tester.upgradeSystem(version);
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.configServer, version, zone1);
- assertWantedVersion(SystemApplication.proxy, version, zone1);
-
- // Controller is downgraded
- tester.upgradeController(Version.fromString("6.4"));
-
- // Wanted version for zone remains unchanged
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.configServer, version, zone1);
- assertWantedVersion(SystemApplication.proxy, version, zone1);
- }
-
- @Test
- void upgrade_halts_on_broken_version() {
- SystemUpgrader systemUpgrader = systemUpgrader(UpgradePolicy.builder().upgrade(zone1).upgrade(zone2).build());
-
- // Initial system version
- Version version1 = Version.fromString("6.5");
- tester.upgradeSystem(version1);
- systemUpgrader.maintain();
- assertCurrentVersion(List.of(SystemApplication.configServerHost, SystemApplication.proxyHost,
- SystemApplication.configServer, SystemApplication.proxy),
- version1, zone1);
- assertCurrentVersion(List.of(SystemApplication.configServerHost, SystemApplication.proxyHost,
- SystemApplication.configServer, SystemApplication.proxy),
- version1, zone2);
-
- // System starts upgrading to next version
- Version version2 = Version.fromString("6.6");
- tester.upgradeController(version2);
- systemUpgrader.maintain();
- completeUpgrade(List.of(SystemApplication.configServerHost, SystemApplication.proxyHost), version2, zone1);
- systemUpgrader.maintain();
- completeUpgrade(SystemApplication.configServer, version2, zone1);
- systemUpgrader.maintain();
- completeUpgrade(SystemApplication.proxy, version2, zone1);
- convergeServices(SystemApplication.proxy, zone1);
-
- // Confidence is reduced to broken and next zone is not scheduled for upgrade
- overrideConfidence(version2, VespaVersion.Confidence.broken);
- systemUpgrader.maintain();
- assertWantedVersion(List.of(SystemApplication.configServerHost, SystemApplication.proxyHost,
- SystemApplication.configServer, SystemApplication.proxy), version1, zone2);
- }
-
- @Test
- void does_not_deploy_proxy_app_in_zone_without_shared_routing() {
- var applications = List.of(SystemApplication.configServerHost, SystemApplication.configServer,
- SystemApplication.tenantHost);
- tester.configServer().bootstrap(List.of(zone1.getId()), applications);
- tester.configServer().disallowConvergenceCheck(SystemApplication.proxy.id());
- tester.zoneRegistry().exclusiveRoutingIn(zone1);
- var systemUpgrader = systemUpgrader(UpgradePolicy.builder().upgrade(zone1).build());
-
- // System begins upgrade
- var version1 = Version.fromString("6.5");
- tester.upgradeController(version1);
- systemUpgrader.maintain();
- assertWantedVersion(applications, version1, zone1);
- assertWantedVersion(SystemApplication.proxy, Version.emptyVersion, zone1);
-
- // System completes upgrade. Wanted version is not raised for proxy as it's is never deployed
- completeUpgrade(applications, version1, zone1);
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.proxy, Version.emptyVersion, zone1);
- tester.computeVersionStatus();
- assertEquals(version1, tester.controller().readSystemVersion());
- }
-
- @Test
- void downgrade_from_aborted_version() {
- SystemUpgrader systemUpgrader = systemUpgrader(UpgradePolicy.builder().upgrade(zone1).upgrade(zone2).upgrade(zone3).build());
-
- Version version1 = Version.fromString("6.5");
- tester.configServer().bootstrap(List.of(zone1.getId(), zone2.getId(), zone3.getId()), SystemApplication.notController());
- tester.upgradeSystem(version1);
- systemUpgrader.maintain();
- assertCurrentVersion(SystemApplication.notController(), version1, zone1, zone2, zone3);
-
- // Controller upgrades
- Version version2 = Version.fromString("6.6");
- tester.upgradeController(version2);
- assertControllerVersion(version2);
-
- // 2/3 zones upgrade
- for (var zone : List.of(zone1, zone2)) {
- systemUpgrader.maintain();
- completeUpgrade(List.of(SystemApplication.tenantHost,
- SystemApplication.proxyHost,
- SystemApplication.configServerHost),
- version2, zone);
- completeUpgrade(SystemApplication.configServer, version2, zone);
- systemUpgrader.maintain();
- completeUpgrade(SystemApplication.proxy, version2, zone);
- convergeServices(SystemApplication.proxy, zone);
- }
-
- // Upgrade is aborted
- overrideConfidence(version2, VespaVersion.Confidence.aborted);
-
- // Dependency graph is inverted and applications without dependencies downgrade first. Upgrade policy is
- // also followed in inverted order
- for (var zone : List.of(zone2, zone1)) {
- systemUpgrader.maintain();
- completeUpgrade(List.of(SystemApplication.tenantHost,
- SystemApplication.configServerHost,
- SystemApplication.proxyHost,
- SystemApplication.proxy),
- version1, zone);
- convergeServices(SystemApplication.proxy, zone);
- List<SystemApplication> lastToDowngrade = List.of(SystemApplication.configServer);
- assertWantedVersion(lastToDowngrade, version2, zone);
-
- // ... and then configserver and proxyhost
- systemUpgrader.maintain();
- completeUpgrade(lastToDowngrade, version1, zone);
- }
- assertSystemVersion(version1);
-
- // Another version is released and system upgrades
- Version version3 = Version.fromString("6.7");
- tester.upgradeSystem(version3);
- assertEquals(version3, tester.controller().readSystemVersion());
-
- // Attempt to abort current system version is rejected
- try {
- overrideConfidence(version3, VespaVersion.Confidence.aborted);
- fail("Expected exception");
- } catch (IllegalArgumentException ignored) {
- }
- systemUpgrader.maintain();
- assertWantedVersion(SystemApplication.notController(), version3, zone1, zone2, zone3);
- }
-
- private void overrideConfidence(Version version, VespaVersion.Confidence confidence) {
- new Upgrader(tester.controller(), Duration.ofDays(1)).overrideConfidence(version, confidence);
- tester.computeVersionStatus();
- }
-
- private void completeUpgrade(SystemApplication application, Version version, ZoneApi first, ZoneApi... rest) {
- completeUpgrade(-1, application, version, first, rest);
- }
-
- /** Simulate upgrade of nodes allocated to given application. In a real system this is done by the node itself */
- private void completeUpgrade(int nodeCount, SystemApplication application, Version version, ZoneApi first, ZoneApi... rest) {
- assertWantedVersion(application, version, first, rest);
- Stream.concat(Stream.of(first), Stream.of(rest)).forEach(zone -> {
- int nodesUpgraded = 0;
- List<Node> nodes = listNodes(zone, application);
- for (Node node : nodes) {
- if (node.currentVersion().equals(node.wantedVersion())) continue;
- nodeRepository().putNodes(
- zone.getId(),
- Node.builder(node).currentVersion(node.wantedVersion()).build());
- if (++nodesUpgraded == nodeCount) {
- break;
- }
- }
- if (nodesUpgraded == nodes.size()) {
- assertCurrentVersion(application, version, zone);
- }
- });
- }
-
- private void convergeServices(SystemApplication application, ZoneApi first, ZoneApi... rest) {
- Stream.concat(Stream.of(first), Stream.of(rest)).forEach(zone -> {
- tester.configServer().convergeServices(application.id(), zone.getId());
- });
- }
-
- private void completeUpgrade(List<SystemApplication> applications, Version version, ZoneApi zone, ZoneApi... rest) {
- applications.forEach(application -> completeUpgrade(application, version, zone, rest));
- }
-
- private void failNodeIn(ZoneApi zone, SystemApplication application) {
- List<Node> nodes = nodeRepository().list(zone.getId(), NodeFilter.all().applications(application.id()));
- if (nodes.isEmpty()) {
- throw new IllegalArgumentException("No nodes allocated to " + application.id());
- }
- Node node = nodes.get(0);
- nodeRepository().putNodes(
- zone.getId(),
- Node.builder(node).state(Node.State.failed).build());
- }
-
- private void assertSystemVersion(Version version) {
- assertEquals(version, tester.controller().readSystemVersion());
- }
-
- private void assertControllerVersion(Version version) {
- assertEquals(version, tester.controller().readVersionStatus().controllerVersion().get().versionNumber());
- }
-
- private void assertWantedVersion(SystemApplication application, Version version, ZoneApi first, ZoneApi... rest) {
- Stream.concat(Stream.of(first), Stream.of(rest)).forEach(zone -> {
- if (!application.hasApplicationPackage()) {
- assertEquals(version,
- nodeRepository().targetVersionsOf(zone.getId()).vespaVersion(application.nodeType()).orElse(Version.emptyVersion),
- "Target version set for " + application + " in " + zone.getId());
- }
- assertVersion(application, version, Node::wantedVersion, zone);
- });
- }
-
- private void assertCurrentVersion(SystemApplication application, Version version, ZoneApi first, ZoneApi... rest) {
- assertVersion(application, version, Node::currentVersion, first, rest);
- }
-
- private void assertWantedVersion(List<SystemApplication> applications, Version version, ZoneApi first, ZoneApi... rest) {
- applications.forEach(application -> assertWantedVersion(application, version, first, rest));
- }
-
- private void assertCurrentVersion(List<SystemApplication> applications, Version version, ZoneApi first, ZoneApi... rest) {
- applications.forEach(application -> assertVersion(application, version, Node::currentVersion, first, rest));
- }
-
- private void assertVersion(SystemApplication application, Version version, Function<Node, Version> versionField,
- ZoneApi first, ZoneApi... rest) {
- Stream.concat(Stream.of(first), Stream.of(rest)).forEach(zone -> {
- for (Node node : listNodes(zone, application)) {
- assertEquals(version, versionField.apply(node), "Version of " + application.id() + " in " + zone.getId());
- }
- });
- }
-
- private List<Node> listNodes(ZoneApi zone, SystemApplication application) {
- return nodeRepository().list(zone.getId(), NodeFilter.all().applications(application.id())).stream()
- .filter(SystemUpgrader::eligibleForUpgrade)
- .toList();
- }
-
- private NodeRepositoryMock nodeRepository() {
- return tester.configServer().nodeRepository();
- }
-
- private SystemUpgrader systemUpgrader(UpgradePolicy upgradePolicy) {
- tester.zoneRegistry().setUpgradePolicy(upgradePolicy);
- return new SystemUpgrader(tester.controller(), Duration.ofDays(1));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainerTest.java
deleted file mode 100644
index 94c2448e6cc..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/TenantRoleMaintainerTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.aws.MockRoleService;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mortent
- */
-public class TenantRoleMaintainerTest {
-
- private final DeploymentTester tester = new DeploymentTester();
-
- @Test
- void maintains_iam_roles_for_tenants_in_production() {
- var devAppTenant1 = tester.newDeploymentContext("tenant1", "app1", "default");
- var prodAppTenant2 = tester.newDeploymentContext("tenant2", "app2", "default");
- var devAppTenant2 = tester.newDeploymentContext("tenant2", "app3", "default");
- var perfAppTenant1 = tester.newDeploymentContext("tenant3", "app1", "default");
- ApplicationPackage appPackage = new ApplicationPackageBuilder()
- .region("us-west-1")
- .build();
-
- // Deploy dev apps
- devAppTenant1.runJob(DeploymentContext.devUsEast1, appPackage);
- devAppTenant2.runJob(DeploymentContext.devUsEast1, appPackage);
-
- // Deploy perf apps
- perfAppTenant1.runJob(DeploymentContext.perfUsEast3, appPackage);
-
- // Deploy prod
- prodAppTenant2.submit(appPackage).deploy();
- assertEquals(1, permanentDeployments(devAppTenant1.instance()));
- assertEquals(1, permanentDeployments(devAppTenant2.instance()));
- assertEquals(1, permanentDeployments(prodAppTenant2.instance()));
-
- var maintainer = new TenantRoleMaintainer(tester.controller(), Duration.ofDays(1));
- maintainer.maintain();
-
- var roleService = tester.controller().serviceRegistry().roleService();
- List<TenantName> tenantNames = ((MockRoleService) roleService).maintainedTenants();
-
- assertTrue(tenantNames.containsAll(List.of(prodAppTenant2.application().id().tenant(), perfAppTenant1.application().id().tenant())));
- }
-
- @Test
- void maintain_batch() {
- var tenants = List.of(
- tester.newDeploymentContext("tenant1", "app1", "default"),
- tester.newDeploymentContext("tenant2", "app1", "default"),
- tester.newDeploymentContext("tenant3", "app1", "default"),
- tester.newDeploymentContext("tenant4", "app1", "default"),
- tester.newDeploymentContext("tenant5", "app1", "default"),
- tester.newDeploymentContext("tenant6", "app1", "default"));
-
- var maintainer = new TenantRoleMaintainer(tester.controller(), Duration.ofDays(1));
- maintainer.maintain();
-
- var maintainedTenants = tester.controller().tenants().asList().stream()
- .filter(t -> t.tenantRolesLastMaintained() != Instant.EPOCH)
- .toList();
-
- var unmaintainedTenants = tester.controller().tenants().asList().stream()
- .filter(t -> t.tenantRolesLastMaintained() == Instant.EPOCH)
- .toList();
-
- assertEquals(5, maintainedTenants.size());
- assertEquals(1, unmaintainedTenants.size());
-
- tester.clock().advance(Duration.ofDays(1));
-
- maintainer.maintain();
- var result = tester.controller().tenants().asList().stream()
- .collect(Collectors.groupingBy(Tenant::tenantRolesLastMaintained));
-
- assertFalse(result.containsKey(Instant.EPOCH));
- }
-
- private long permanentDeployments(Instance instance) {
- return tester.controller().applications().requireInstance(instance.id()).deployments().values().stream()
- .filter(deployment -> !deployment.zone().environment().isTest())
- .count();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
deleted file mode 100644
index c8853c008f7..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UpgraderTest.java
+++ /dev/null
@@ -1,1168 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Random;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.devUsEast1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsCentral1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsEast3;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.stagingTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.systemTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.ALL;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.PIN;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel.PLATFORM;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author bratseth
- */
-public class UpgraderTest {
-
- private final DeploymentTester tester = new DeploymentTester().atMondayMorning();
-
- @Test
- void testUpgrading() {
- // --- Setup
- Version version0 = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version0);
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(0, tester.jobs().active().size(), "No applications: Nothing to do");
-
- // Setup applications
- var canary0 = createAndDeploy("canary0", "canary");
- var canary1 = createAndDeploy("canary1", "canary");
- var default0 = createAndDeploy("default0", "default");
- var default1 = createAndDeploy("default1", "default");
- var default2 = createAndDeploy("default2", "default");
- var conservative0 = createAndDeploy("conservative0", "conservative");
-
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(0, tester.jobs().active().size(), "All already on the right version: Nothing to do");
-
- // --- Next version released - everything goes smoothly
- Version version1 = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version1);
- assertEquals(version1, tester.controller().readVersionStatus().systemVersion().get().versionNumber());
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- assertEquals(4, tester.jobs().active().size(), "New system version: Should upgrade Canaries");
- canary0.deployPlatform(version1);
- assertEquals(version1, tester.configServer().lastPrepareVersion().get());
-
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size(), "One canary pending; nothing else");
-
- canary1.deployPlatform(version1);
-
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.normal, tester.controller().readVersionStatus().systemVersion().get().confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(6, tester.jobs().active().size(), "Canaries done: Should upgrade defaults");
-
- default0.deployPlatform(version1);
- default1.deployPlatform(version1);
- default2.deployPlatform(version1);
-
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.high, tester.controller().readVersionStatus().systemVersion().get().confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size(), "Normals done: Should upgrade conservatives");
- conservative0.deployPlatform(version1);
-
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(0, tester.jobs().active().size(), "Nothing to do");
-
- // --- Next version released - which fails a Canary
- Version version2 = Version.fromString("6.4");
- tester.controllerTester().upgradeSystem(version2);
- assertEquals(version2, tester.controller().readVersionStatus().systemVersion().get().versionNumber());
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- assertEquals(4, tester.jobs().active().size(), "New system version: Should upgrade Canaries");
- canary0.runJob(systemTest);
- canary0.failDeployment(stagingTest);
-
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.broken, tester.controller().readVersionStatus().systemVersion().get().confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(3, tester.jobs().active().size(), "Version broken, but Canaries should keep trying");
-
- // --- Next version released - which repairs the Canary app and fails a default
- Version version3 = Version.fromString("6.5");
- tester.controllerTester().upgradeSystem(version3);
- assertEquals(version3, tester.controller().readVersionStatus().systemVersion().get().versionNumber());
- tester.upgrader().maintain();
- canary0.abortJob(stagingTest);
- canary1.abortJob(systemTest);
- canary1.abortJob(stagingTest);
- tester.triggerJobs();
-
- assertEquals(4, tester.jobs().active().size(), "New system version: Should upgrade Canaries");
- canary0.deployPlatform(version3);
- assertEquals(version3, tester.configServer().lastPrepareVersion().get());
-
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size(), "One canary pending; nothing else");
-
- canary1.deployPlatform(version3);
-
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.normal, tester.controller().readVersionStatus().systemVersion().get().confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- assertEquals(6, tester.jobs().active().size(), "Canaries done: Should upgrade defaults");
-
- default0.runJob(systemTest);
- default0.failDeployment(stagingTest);
- default1.deployPlatform(version3);
- default2.deployPlatform(version3);
-
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.normal, tester.controller().readVersionStatus().systemVersion().get().confidence(), "Not enough evidence to mark this as neither broken nor high");
-
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size(), "Upgrade with error should retry");
-
- // --- Failing application is repaired by changing the application, causing confidence to move above 'high' threshold
- // Deploy application change
- default0.submit(applicationPackage("default"));
- default0.runJob(systemTest)
- .jobAborted(stagingTest) // New revision causes run with failing upgrade alone to be aborted.
- .runJob(stagingTest)
- .deploy();
-
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.high, tester.controller().readVersionStatus().systemVersion().get().confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size(), "Normals done: Should upgrade conservatives");
- conservative0.deployPlatform(version3);
-
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(0, tester.jobs().active().size(), "Applications are on " + version3 + " - nothing to do");
-
- // --- Starting upgrading to a new version which breaks, causing upgrades to commence on the previous version
- var default3 = createAndDeploy("default3", "default");
- var default4 = createAndDeploy("default4", "default");
- var default5 = createAndDeploy("default5", "default");
- Version version4 = Version.fromString("6.6");
- tester.controllerTester().upgradeSystem(version4);
- tester.upgrader().maintain(); // cause canary upgrades to new version
- tester.triggerJobs();
- canary0.deployPlatform(version4);
- canary1.deployPlatform(version4);
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.normal, tester.controller().readVersionStatus().systemVersion().get().confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- assertEquals(12, tester.jobs().active().size(), "Upgrade of defaults are scheduled");
- for (var context : List.of(default0, default1, default2, default3, default4, default5))
- assertEquals(version4, context.instance().change().platform().get());
-
- default0.deployPlatform(version4);
-
- // State: Default applications started upgrading to version4 (and one completed)
- Version version5 = Version.fromString("6.7");
- tester.controllerTester().upgradeSystem(version5);
- tester.upgrader().maintain(); // cause canary upgrades to version5
- tester.triggerJobs();
- canary0.deployPlatform(version5);
- canary1.deployPlatform(version5);
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.normal, tester.controller().readVersionStatus().systemVersion().get().confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- assertEquals(12, tester.jobs().active().size(), "Upgrade of defaults are scheduled");
- assertEquals(version5, default0.instance().change().platform().get());
- for (var context : List.of(default1, default2, default3, default4, default5))
- assertEquals(version4, context.instance().change().platform().get());
-
- default1.deployPlatform(version4);
- default2.deployPlatform(version4);
-
- default3.runJob(systemTest)
- .failDeployment(stagingTest);
-
- default4.runJob(systemTest)
- .runJob(stagingTest)
- .failDeployment(productionUsWest1);
-
- default5.runJob(systemTest)
- .runJob(stagingTest)
- .failDeployment(productionUsWest1);
-
- // State: Default applications started upgrading to version5
- tester.clock().advance(Duration.ofHours(1));
- tester.upgrader().maintain();
- default3.triggerJobs().jobAborted(stagingTest);
- default0.runJob(systemTest)
- .failDeployment(stagingTest);
- default1.runJob(systemTest)
- .failDeployment(stagingTest);
- default2.runJob(systemTest)
- .failDeployment(stagingTest);
-
- default3.runJob(systemTest)
- .runJob(stagingTest)
- .failDeployment(productionUsWest1);
- default4.failDeployment(systemTest);
- default5.failDeployment(systemTest);
-
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.broken, tester.controller().readVersionStatus().systemVersion().get().confidence());
-
- tester.upgrader().maintain();
- assertEquals(version4, default3.instance().change().platform().get());
- }
-
- @Test
- void testUpgradingToVersionWhichBreaksSomeNonCanaries() {
- // --- Setup
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(0, tester.jobs().active().size(), "No system version: Nothing to do");
-
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(0, tester.jobs().active().size(), "No applications: Nothing to do");
-
- // Setup applications
- var canary0 = createAndDeploy("canary0", "canary");
- var canary1 = createAndDeploy("canary1", "canary");
- var default0 = createAndDeploy("default0", "default");
- var default1 = createAndDeploy("default1", "default");
- var default2 = createAndDeploy("default2", "default");
- var default3 = createAndDeploy("default3", "default");
- var default4 = createAndDeploy("default4", "default");
- var default5 = createAndDeploy("default5", "default");
- var default6 = createAndDeploy("default6", "default");
- var default7 = createAndDeploy("default7", "default");
- var default8 = createAndDeploy("default8", "default");
- var default9 = createAndDeploy("default9", "default");
-
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(0, tester.jobs().active().size(), "All already on the right version: Nothing to do");
-
- // --- A new version is released
- version = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version);
- assertEquals(version, tester.controller().readVersionStatus().systemVersion().get().versionNumber());
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- assertEquals(4, tester.jobs().active().size(), "New system version: Should upgrade Canaries");
- canary0.deployPlatform(version);
- assertEquals(version, tester.configServer().lastPrepareVersion().get());
-
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(2, tester.jobs().active().size(), "One canary pending; nothing else");
-
- canary1.deployPlatform(version);
-
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.normal, tester.controller().readVersionStatus().systemVersion().get().confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(20, tester.jobs().active().size(), "Canaries done: Should upgrade defaults");
-
- default0.deployPlatform(version);
- for (var context : List.of(default1, default2, default3, default4, default5, default6))
- context.failDeployment(systemTest);
-
- // > 60% and at least 6 failed - version is broken
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- tester.abortAll();
- tester.triggerJobs();
- assertEquals(VespaVersion.Confidence.broken, tester.controller().readVersionStatus().systemVersion().get().confidence());
- assertEquals(0, tester.jobs().active().size(), "Upgrades are cancelled");
- }
-
- // Scenario:
- // An application A is on version V0
- // Version V2 is released.
- // A upgrades one production zone to V2.
- // V2 is marked as broken and upgrade of A to V2 is cancelled.
- // Upgrade of A to V1 is scheduled: Should skip the zone on V2 but upgrade the next zone to V1
- @Test
- void testVersionIsBrokenAfterAZoneIsLive() {
- Version v0 = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(v0);
-
- // Setup applications on V0
- var canary0 = createAndDeploy("canary0", "canary");
- var canary1 = createAndDeploy("canary1", "canary");
- var default0 = createAndDeploy("default0", "default");
- var default1 = createAndDeploy("default1", "default");
- var default2 = createAndDeploy("default2", "default");
- var default3 = createAndDeploy("default3", "default");
- var default4 = createAndDeploy("default4", "default");
- var default5 = createAndDeploy("default5", "default");
- var default6 = createAndDeploy("default6", "default");
-
- // V1 is released
- Version v1 = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(v1);
- assertEquals(v1, tester.controller().readVersionStatus().systemVersion().get().versionNumber());
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- // Canaries upgrade and raise confidence of V1 (other apps are not upgraded)
- canary0.deployPlatform(v1);
- canary1.deployPlatform(v1);
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.normal, tester.controller().readVersionStatus().systemVersion().get().confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- // V2 is released
- Version v2 = Version.fromString("6.4");
- tester.controllerTester().upgradeSystem(v2);
- assertEquals(v2, tester.controller().readVersionStatus().systemVersion().get().versionNumber());
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- // Canaries upgrade and raise confidence of V2
- canary0.deployPlatform(v2);
- canary1.deployPlatform(v2);
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.normal, tester.controller().readVersionStatus().systemVersion().get().confidence());
-
- // We "manually" cancel upgrades to V1 so that we can use the applications to make V2 fail instead
- // But we keep one (default6) to avoid V1 being garbage collected
- tester.deploymentTrigger().cancelChange(default0.instanceId(), ALL);
- tester.deploymentTrigger().cancelChange(default1.instanceId(), ALL);
- tester.deploymentTrigger().cancelChange(default2.instanceId(), ALL);
- tester.deploymentTrigger().cancelChange(default3.instanceId(), ALL);
- tester.deploymentTrigger().cancelChange(default4.instanceId(), ALL);
- tester.deploymentTrigger().cancelChange(default5.instanceId(), ALL);
- default0.abortJob(systemTest).abortJob(stagingTest);
- default1.abortJob(systemTest).abortJob(stagingTest);
- default2.abortJob(systemTest).abortJob(stagingTest);
- default3.abortJob(systemTest).abortJob(stagingTest);
- default4.abortJob(systemTest).abortJob(stagingTest);
- default5.abortJob(systemTest).abortJob(stagingTest);
-
- // Applications with default policy start upgrading to V2
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(14, tester.jobs().active().size(), "Upgrade scheduled for remaining apps");
- assertEquals(v1, default6.instance().change().platform().get(), "default6 is still upgrading to 6.3");
-
- // 4/5 applications fail (in the last prod zone) and lowers confidence
- default0.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1).failDeployment(productionUsEast3);
- default1.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1).failDeployment(productionUsEast3);
- default2.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1).failDeployment(productionUsEast3);
- default3.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1).failDeployment(productionUsEast3);
- default4.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1).failDeployment(productionUsEast3);
- default5.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1).failDeployment(productionUsEast3);
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.broken, tester.controller().readVersionStatus().systemVersion().get().confidence());
-
- assertEquals(v2, default0.deployment(ZoneId.from("prod.us-west-1")).version());
- assertEquals(v0, default0.deployment(ZoneId.from("prod.us-east-3")).version());
- tester.upgrader().maintain();
- tester.abortAll();
- tester.triggerJobs();
-
- assertEquals(14, tester.jobs().active().size(), "Upgrade to 5.1 scheduled for apps not completely on 5.1 or 5.2");
-
- // prod zone on 5.2 (usWest1) is skipped, but we still trigger the next zone from triggerReadyJobs:
- default0.runJob(systemTest).runJob(stagingTest).runJob(productionUsEast3);
-
- // Resulting state:
- assertEquals(v2, default0.deployment(ZoneId.from("prod.us-west-1")).version());
- assertEquals(v1, default0.deployment(ZoneId.from("prod.us-east-3")).version(), "Last zone is upgraded to v1");
- assertFalse(default0.instance().change().hasTargets());
- }
-
- @Test
- void testConfidenceIgnoresFailingApplicationChanges() {
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
-
- // Setup applications
- var canary0 = createAndDeploy("canary0", "canary");
- var canary1 = createAndDeploy("canary1", "canary");
- var default0 = createAndDeploy("default0", "default");
- var default1 = createAndDeploy("default1", "default");
- var default2 = createAndDeploy("default2", "default");
- var default3 = createAndDeploy("default3", "default");
- var default4 = createAndDeploy("default4", "default");
-
- // New version is released
- version = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version);
- assertEquals(version, tester.controller().readVersionStatus().systemVersion().get().versionNumber());
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- // Canaries upgrade and raise confidence
- canary0.deployPlatform(version);
- canary1.deployPlatform(version);
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.normal, tester.controller().readVersionStatus().systemVersion().get().confidence());
-
- // All applications upgrade successfully
- tester.upgrader().maintain();
- tester.triggerJobs();
- for (var context : List.of(default0, default1, default2, default3, default4))
- context.deployPlatform(version);
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.high, tester.controller().readVersionStatus().systemVersion().get().confidence());
-
- // Multiple application changes are triggered and fail, but does not affect version confidence as upgrade has
- // completed successfully
- ApplicationPackage applicationPackage = applicationPackage("default");
- default0.submit(applicationPackage).failDeployment(systemTest);
- default1.submit(applicationPackage).failDeployment(stagingTest);
- default2.submit(applicationPackage).failDeployment(systemTest);
- default3.submit(applicationPackage).failDeployment(stagingTest);
- tester.controllerTester().computeVersionStatus();
- assertEquals(VespaVersion.Confidence.high, tester.controller().readVersionStatus().systemVersion().get().confidence());
- }
-
- @Test
- void testBlockVersionChange() {
- // Tuesday, 18:00
- tester.at(Instant.parse("2017-09-26T18:00:00.00Z"));
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
-
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .upgradePolicy("canary")
- // Block upgrades on Tuesday in hours 18 and 19
- .blockChange(false, true, "tue", "18-19", "UTC")
- .region("us-west-1")
- .build();
-
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- // New version is released
- version = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version);
-
- // Application is not upgraded at this time
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertTrue(tester.jobs().active().isEmpty(), "No jobs scheduled");
-
- // One hour passes, time is 19:00, still no upgrade
- tester.clock().advance(Duration.ofHours(1));
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertTrue(tester.jobs().active().isEmpty(), "No jobs scheduled");
-
- // Two hours pass in total, time is 20:00 and application upgrades
- tester.clock().advance(Duration.ofHours(1));
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertFalse(tester.jobs().active().isEmpty(), "Job is scheduled");
- app.deployPlatform(version);
- assertTrue(tester.jobs().active().isEmpty(), "All jobs consumed");
- }
-
- @Test
- void testBlockVersionChangeHalfwayThough() {
- // Tuesday, 17:00
- tester.at(Instant.parse("2017-09-26T17:00:00.00Z"));
-
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
-
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .upgradePolicy("canary")
- // Block upgrades on Tuesday in hours 18 and 19
- .blockChange(false, true, "tue", "18-19", "UTC")
- .region("us-west-1")
- .region("us-central-1")
- .region("us-east-3")
- .build();
-
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- // New version is released
- version = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version);
-
- // Application upgrade starts
- tester.upgrader().maintain();
- tester.triggerJobs();
- app.runJob(systemTest).runJob(stagingTest);
- tester.triggerJobs();
- tester.clock().advance(Duration.ofHours(1)); // Entering block window after prod job is triggered
- app.runJob(productionUsWest1);
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size()); // Next job triggered because upgrade is already rolling out.
-
- app.runJob(productionUsCentral1).runJob(productionUsEast3);
- assertTrue(tester.jobs().active().isEmpty(), "All jobs consumed");
- }
-
- @Test
- void testBlockVersionChangeHalfwayThroughThenNewRevision() {
- // Friday, 16:00
- tester.at(Instant.parse("2017-09-29T16:00:00.00Z"));
-
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
-
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- // Block upgrades on weekends and outside working hours
- .blockChange(false, true, "mon-fri", "00-09,17-23", "UTC")
- .blockChange(false, true, "sat-sun", "00-23", "UTC")
- .region("us-west-1")
- .region("us-central-1")
- .region("us-east-3")
- .build();
-
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- // New version is released
- version = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version);
-
- // Application upgrade starts
- tester.upgrader().maintain();
- tester.triggerJobs();
- app.runJob(systemTest).runJob(stagingTest);
- tester.clock().advance(Duration.ofHours(1)); // Entering block window after prod job is triggered
- app.runJob(productionUsWest1);
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size()); // Next job triggered, as upgrade is already in progress.
- // us-central-1 fails, permitting a new revision.
- app.failDeployment(productionUsCentral1);
-
- // A new revision is submitted and starts rolling out.
- app.submit(applicationPackage);
-
- // production-us-central-1 is re-triggered with upgrade until revision catches up.
- tester.triggerJobs();
- assertEquals(3, tester.jobs().active().size());
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1);
- // us-central-1 has an older version, and needs a new staging test to begin.
- app.runJob(stagingTest).triggerJobs().jobAborted(productionUsCentral1); // Retry will include revision.
- tester.triggerJobs(); // Triggers us-central-1 before platform upgrade is cancelled.
-
- // A new version is also released, and someone cancels the upgrade, suspecting it is faulty.
- tester.clock().advance(Duration.ofHours(17));
- version = Version.fromString("6.4");
- tester.controllerTester().upgradeSystem(version);
- tester.upgrader().maintain();
- tester.deploymentTrigger().cancelChange(app.instanceId(), PLATFORM);
-
- // us-central-1 succeeds upgrade to 6.3, with the revision, but us-east-3 wants to proceed with only the revision change.
- app.runJob(productionUsCentral1);
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsEast3);
- assertEquals(List.of(), tester.jobs().active());
-
- assertEquals(new Version("6.3"), app.deployment(ZoneId.from("prod", "us-central-1")).version());
- assertEquals(new Version("6.2"), app.deployment(ZoneId.from("prod", "us-east-3")).version());
-
- // Monday morning: We are not blocked, and the new version rolls out to all zones.
- tester.clock().advance(Duration.ofDays(2)); // Monday, 10:00
- tester.upgrader().maintain();
- tester.triggerJobs();
- app.runJob(systemTest)
- .runJob(stagingTest)
- .runJob(productionUsWest1)
- .runJob(productionUsCentral1)
- .runJob(stagingTest)
- .runJob(productionUsEast3);
- assertTrue(tester.jobs().active().isEmpty(), "All jobs consumed");
-
- // App is completely upgraded to the latest version
- for (Deployment deployment : app.instance().deployments().values())
- assertEquals(version, deployment.version());
- }
-
- @Test
- void testThrottlesUpgrades() {
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
-
- // Setup our own upgrader as we need to control the interval
- Upgrader upgrader = new Upgrader(tester.controller(), Duration.ofMinutes(10));
- upgrader.setUpgradesPerMinute(0.2);
-
- // Setup applications
- var canary0 = createAndDeploy("canary0", "canary");
- var canary1 = createAndDeploy("canary1", "canary");
- var canary2 = createAndDeploy("canary2", "canary");
- var default0 = createAndDeploy("default0", "default");
- var default1 = createAndDeploy("default1", "default");
- var default2 = createAndDeploy("default2", "default");
- var default3 = createAndDeploy("default3", "default");
-
- // Dev deployment which should be ignored
- var dev0 = tester.newDeploymentContext("tenant1", "dev0", "default")
- .runJob(devUsEast1, DeploymentContext.applicationPackage());
-
- // New version is released and canaries upgrade
- version = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version);
- assertEquals(version, tester.controller().readVersionStatus().systemVersion().get().versionNumber());
- upgrader.maintain();
- tester.triggerJobs();
-
- // canaries are not throttled
- assertEquals(6, tester.jobs().active().size());
- canary0.deployPlatform(version);
- canary1.deployPlatform(version);
- canary2.deployPlatform(version);
- assertEquals(0, tester.jobs().active().size());
- tester.controllerTester().computeVersionStatus();
-
- // Next run upgrades a subset
- upgrader.maintain();
- tester.triggerJobs();
- assertEquals(4, tester.jobs().active().size());
-
- // Remaining applications upgraded
- upgrader.maintain();
- tester.triggerJobs();
- assertEquals(8, tester.jobs().active().size());
- }
-
- @Test
- void testPinningMajorVersionInDeploymentXml() {
- Version version0 = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version0);
-
- ApplicationPackageBuilder builder = new ApplicationPackageBuilder().region("us-west-1").majorVersion(7);
- ApplicationPackage defaultPackage = new ApplicationPackageBuilder().region("us-west-1").build();
-
- // Setup applications
- var canaryApp = tester.newDeploymentContext("canary", "app", "default").submit(builder.upgradePolicy("canary").build()).deploy();
- var defaultApp = tester.newDeploymentContext("normal", "app", "default").submit(builder.upgradePolicy("default").build()).deploy();
- var conservativeApp = tester.newDeploymentContext("conservative", "app", "default").submit(builder.upgradePolicy("conservative").build()).deploy();
- var lazyApp = tester.newDeploymentContext().submit(defaultPackage).deploy();
-
- // New major version is released; more apps upgrade with increasing confidence.
- Version version = Version.fromString("7.0");
- tester.controllerTester().upgradeSystem(version);
- tester.upgrader().overrideConfidence(version, Confidence.broken);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- assertEquals(Change.of(version), canaryApp.instance().change());
- assertEquals(Change.empty(), defaultApp.instance().change());
- assertEquals(Change.empty(), conservativeApp.instance().change());
- assertEquals(Change.empty(), lazyApp.instance().change());
-
- tester.upgrader().overrideConfidence(version, Confidence.low);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- assertEquals(Change.of(version), canaryApp.instance().change());
- assertEquals(Change.empty(), defaultApp.instance().change());
- assertEquals(Change.empty(), conservativeApp.instance().change());
- assertEquals(Change.empty(), lazyApp.instance().change());
-
- tester.upgrader().overrideConfidence(version, Confidence.normal);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- assertEquals(Change.of(version), canaryApp.instance().change());
- assertEquals(Change.of(version), defaultApp.instance().change());
- assertEquals(Change.empty(), conservativeApp.instance().change());
- assertEquals(Change.empty(), lazyApp.instance().change());
-
- tester.upgrader().overrideConfidence(version, Confidence.high);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- assertEquals(Change.of(version), canaryApp.instance().change());
- assertEquals(Change.of(version), defaultApp.instance().change());
- assertEquals(Change.of(version), conservativeApp.instance().change());
- assertEquals(Change.empty(), lazyApp.instance().change());
- }
-
- @Test
- void testAllowApplicationChangeDuringFailingUpgrade() {
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
-
- var app = createAndDeploy("app", "default");
-
- // New version is released
- version = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version);
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- app.runJob(systemTest).runJob(stagingTest).failDeployment(productionUsWest1);
-
- // New application change
- app.submit(applicationPackage("default"));
- RevisionId revision = app.lastSubmission().get();
-
- // Application change recorded together with ongoing upgrade
- assertTrue(app.instance().change().platform().get().equals(version) &&
- app.instance().change().revision().get().equals(revision),
- "Change contains both upgrade and application change");
-
- // Deployment completes
- app.runJob(systemTest).runJob(stagingTest)
- .runJob(productionUsWest1)
- .runJob(productionUsEast3);
- assertEquals(List.of(), tester.jobs().active(), "All jobs consumed");
-
- for (Deployment deployment : app.instance().deployments().values()) {
- assertEquals(version, deployment.version());
- assertEquals(revision, deployment.revision());
- }
- }
-
- @Test
- void testBlockRevisionChangeHalfwayThoughThenUpgrade() {
- // Tuesday, 17:00.
- tester.at(Instant.parse("2017-09-26T17:00:00.00Z"));
-
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
-
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .upgradePolicy("canary")
- // Block revisions on Tuesday in hours 18 and 19.
- .blockChange(true, false, "tue", "18-19", "UTC")
- .region("us-west-1")
- .region("us-central-1")
- .region("us-east-3")
- .build();
-
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- // Application upgrade starts.
- app.submit(applicationPackage);
- tester.clock().advance(Duration.ofHours(1)); // Entering block window after submission accepted.
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1);
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size()); // Next job triggered in spite of block, because it is already rolling out.
-
- // New version is released, but upgrades won't start since there's already a revision rolling out.
- version = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version);
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size()); // Still just the revision upgrade.
-
- app.runJob(productionUsCentral1).runJob(productionUsEast3);
- assertEquals(List.of(), tester.jobs().active()); // No jobs left.
-
- // Upgrade may start, now that revision is rolled out.
- tester.upgrader().maintain();
- app.deployPlatform(version);
- assertTrue(tester.jobs().active().isEmpty(), "All jobs consumed");
- }
-
- @Test
- void testBlockRevisionChangeHalfwayThoughThenNewRevision() {
- // Tuesday, 17:00.
- tester.at(Instant.parse("2017-09-26T17:00:00.00Z"));
-
- Version version = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version);
-
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .upgradePolicy("canary")
- // Block revision on Tuesday in hours 18 and 19.
- .blockChange(true, false, "tue", "18-19", "UTC")
- .region("us-west-1")
- .region("us-central-1")
- .region("us-east-3")
- .build();
-
- var app = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
-
- // Application revision starts rolling out.
- app.submit(applicationPackage);
- tester.clock().advance(Duration.ofHours(1)); // Entering block window after submission is accepted.
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1);
- tester.triggerJobs();
- assertEquals(1, tester.jobs().active().size());
-
- // New revision is submitted, but is stored as outstanding, since the upgrade is proceeding in good fashion.
- app.submit(applicationPackage);
- tester.triggerJobs();
- assertEquals(3, tester.jobs().active().size()); // Just the running upgrade, and tests for the new revision.
-
- app.runJob(productionUsCentral1).runJob(productionUsEast3).runJob(systemTest).runJob(stagingTest);
- assertEquals(List.of(), tester.jobs().active()); // No jobs left.
-
- tester.outstandingChangeDeployer().run();
- assertFalse(app.instance().change().hasTargets());
- tester.clock().advance(Duration.ofHours(2));
-
- tester.outstandingChangeDeployer().run();
- assertTrue(app.instance().change().hasTargets());
- app.runJob(productionUsWest1).runJob(productionUsCentral1).runJob(productionUsEast3);
- assertFalse(app.instance().change().hasTargets());
- }
-
- @Test
- void testPinning() {
- Version version0 = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version0);
-
- // Create an application with pinned platform version.
- var context = tester.newDeploymentContext().submit().deploy();
- tester.deploymentTrigger().forceChange(context.instanceId(), Change.empty().withPlatformPin());
-
- assertFalse(context.instance().change().hasTargets());
- assertTrue(context.instance().change().isPlatformPinned());
- assertEquals(3, context.instance().deployments().size());
-
- // Application does not upgrade.
- Version version1 = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- assertFalse(context.instance().change().hasTargets());
- assertTrue(context.instance().change().isPlatformPinned());
-
- // New application package is deployed.
- context.submit().deploy();
- assertFalse(context.instance().change().hasTargets());
- assertTrue(context.instance().change().isPlatformPinned());
-
- // Application upgrades to new version when pin is removed.
- tester.deploymentTrigger().cancelChange(context.instanceId(), PIN);
- tester.upgrader().maintain();
- assertTrue(context.instance().change().hasTargets());
- assertFalse(context.instance().change().isPlatformPinned());
-
- // Application is pinned to new version, and upgrade is therefore not cancelled, even though confidence is broken.
- tester.deploymentTrigger().forceChange(context.instanceId(), Change.empty().withPlatformPin());
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(version1, context.instance().change().platform().get());
-
- // Application fails upgrade after one zone is complete, and is pinned again to the old version.
- context.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1)
- .timeOutUpgrade(productionUsWest1);
- tester.deploymentTrigger().cancelChange(context.instanceId(), ALL);
- tester.deploymentTrigger().forceChange(context.instanceId(), Change.of(version0).withPlatformPin());
- assertEquals(version0, context.instance().change().platform().get());
-
- // Application downgrades to pinned version.
- tester.abortAll();
- context.runJob(stagingTest).runJob(productionUsCentral1).runJob(productionUsWest1);
- assertTrue(context.instance().change().hasTargets());
- context.runJob(productionUsEast3);
- assertFalse(context.instance().change().hasTargets());
- }
-
- @Test
- void upgradesToLatestAllowedMajor() {
- Version version0 = Version.fromString("6.1");
- tester.controllerTester().upgradeSystem(version0);
-
- // All applications deploy on current version
- var app1 = createAndDeploy("app1", "default");
- var app2 = createAndDeploy("app2", "default");
-
- // Keep app 1 on current version
- tester.controller().applications().lockApplicationIfPresent(app1.application().id(), app ->
- tester.controller().applications().store(app.with(app1.instance().name(),
- instance -> instance.withChange(instance.change().withPlatformPin()))));
-
- // New version is released
- Version version1 = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
-
- // App 2 upgrades
- app2.deployPlatform(version1);
-
- // New major version is released
- Version version2 = Version.fromString("7.1");
- tester.controllerTester().upgradeSystem(version2);
-
- // App 2 has upgrade to new platform triggered
- tester.deploymentTrigger().forceChange(app2.instanceId(), Change.of(version2));
- tester.upgrader().maintain();
- assertEquals(Change.of(version2), app2.instance().change());
-
- // App 1 is unpinned and upgrades to latest 6
- tester.controller().applications().lockApplicationIfPresent(app1.application().id(), app ->
- tester.controller().applications().store(app.with(app1.instance().name(),
- instance -> instance.withChange(instance.change().withoutPlatformPin()))));
- tester.upgrader().maintain();
- assertEquals(version1,
- app1.instance().change().platform().orElseThrow(),
- "Application upgrades to latest allowed major");
-
- // Version on old major becomes legacy, so app upgrades once it does not specify the old major in deployment spec.
- app1.runJob(systemTest).runJob(stagingTest).runJob(productionUsWest1).runJob(productionUsEast3);
- app1.submit(new ApplicationPackageBuilder().majorVersion(6).region("us-east-3").region("us-west-1").build()).deploy();
- tester.upgrader().maintain();
- assertEquals(Change.empty(), app1.instance().change());
-
- app1.submit(new ApplicationPackageBuilder().region("us-east-3").region("us-west-1").build()).deploy();
- tester.upgrader().overrideConfidence(version1, Confidence.legacy);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- assertEquals(Change.of(version2), app1.instance().change());
- }
-
- @Test
- void testSettingFailingRevisionAside() {
- DeploymentContext app = tester.newDeploymentContext().submit().deploy();
-
- // New revision fails.
- app.submit();
- Optional<RevisionId> revision1 = app.lastSubmission();
- app.failDeployment(systemTest);
-
- // New version is not targeted.
- Version version1 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version1);
- assertEquals(Change.of(revision1.get()), app.instance().change());
-
- tester.upgrader().run();
- assertEquals(Change.of(revision1.get()), app.instance().change());
-
- // Broken revision is replaced by a new attempt, which also fails, and cancellation is not yet triggered.
- tester.clock().advance(DeploymentTrigger.maxFailingRevisionTime.plusSeconds(1));
- app.submit();
- Optional<RevisionId> revision2 = app.lastSubmission();
- app.failDeployment(systemTest);
- tester.upgrader().run();
- assertEquals(Change.of(revision2.get()), app.instance().change());
-
- // Broken revision is cancelled, and new version targeted, after some time.
- tester.clock().advance(DeploymentTrigger.maxFailingRevisionTime.plusSeconds(1));
- tester.upgrader().run();
- assertEquals(Change.of(version1), app.instance().change());
-
- // Broken revision is not targeted again.
- app.triggerJobs();
- tester.upgrader().run();
- tester.outstandingChangeDeployer().run();
- assertEquals(Change.of(version1), app.instance().change());
-
- app.failDeployment(systemTest);
- tester.upgrader().run();
- tester.outstandingChangeDeployer().run();
- assertEquals(Change.of(version1), app.instance().change());
-
- // Revision gets a second change when upgrade fixes the failing job.
- tester.clock().advance(Duration.ofDays(12)); // Time for retries.
- app.runJob(systemTest).jobAborted(stagingTest).runJob(stagingTest).runJob(productionUsCentral1);
- tester.upgrader().run();
- tester.outstandingChangeDeployer().run();
-
- assertEquals(Change.of(version1).with(revision2.get()), app.instance().change());
- app.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1); // Revision rolls.
- app.runJob(productionUsEast3).runJob(productionUsWest1); // Upgrade completes.
- app.runJob(productionUsEast3).runJob(productionUsWest1); // Revision completes.
- assertEquals(Change.empty(), app.instance().change());
- }
-
- @Test
- void testsEachUpgradeCombinationWithFailingDeployments() {
- Version v1 = Version.fromString("6.1");
- tester.controllerTester().upgradeSystem(v1);
-
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-central-1")
- .region("us-west-1")
- .region("us-east-3")
- .build();
- var application = tester.newDeploymentContext().submit(applicationPackage).deploy();
-
- // Next version is released and 2/3 deployments upgrade
- Version v2 = Version.fromString("6.2");
- tester.controllerTester().upgradeSystem(v2);
- tester.upgrader().maintain();
- assertEquals(Change.of(v2), application.instance().change());
- application.runJob(systemTest).runJob(stagingTest).runJob(productionUsCentral1).timeOutConvergence(productionUsWest1);
- tester.triggerJobs();
-
- // While second deployment completes upgrade, version confidence becomes broken and upgrade is cancelled
- tester.upgrader().overrideConfidence(v2, VespaVersion.Confidence.broken);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- application.runJob(productionUsWest1);
- assertTrue(application.instance().change().isEmpty());
-
- // Next version is released
- Version v3 = Version.fromString("6.3");
- tester.controllerTester().upgradeSystem(v3);
- tester.upgrader().maintain();
- assertEquals(Change.of(v3), application.instance().change());
- application.runJob(systemTest).runJob(stagingTest);
-
- // First deployment starts upgrading
- // Before deployment completes, v1->v3 combination is tested as us-east-3 is still on v1
- tester.triggerJobs();
-
- application.runJob(stagingTest);
- assertEquals(v1, application.instanceJobs().get(stagingTest).lastSuccess().get().versions().sourcePlatform().get());
- assertEquals(v3, application.instanceJobs().get(stagingTest).lastSuccess().get().versions().targetPlatform());
-
- // First deployment fails and then successfully upgrades to v3
- application.failDeployment(productionUsCentral1);
- application.runJob(productionUsCentral1);
-
- // Deployments are now on 3 versions
- assertEquals(v3, application.deployment(ZoneId.from("prod", "us-central-1")).version());
- assertEquals(v2, application.deployment(ZoneId.from("prod", "us-west-1")).version());
- assertEquals(v1, application.deployment(ZoneId.from("prod", "us-east-3")).version());
-
- // Second deployment upgrades
- application.runJob(productionUsWest1);
-
- // Upgrade completes
- application.runJob(productionUsEast3);
- assertTrue(application.instance().change().isEmpty(), "Upgrade complete");
- }
-
- @Test
- void testUpgradesPerMinute() {
- assertEquals(0, Upgrader.numberOfApplicationsToUpgrade(10, 0, 0));
-
- for (long now = 0; now < 60_000; now++)
- assertEquals(7, Upgrader.numberOfApplicationsToUpgrade(60_000, now, 7));
-
- // Upgrade an app after 8s, 16s, ..., 120s.
- assertEquals(3, Upgrader.numberOfApplicationsToUpgrade(30_000, 0, 7.5));
- assertEquals(4, Upgrader.numberOfApplicationsToUpgrade(30_000, 30_000, 7.5));
- assertEquals(4, Upgrader.numberOfApplicationsToUpgrade(30_000, 60_000, 7.5));
- assertEquals(4, Upgrader.numberOfApplicationsToUpgrade(30_000, 90_000, 7.5));
- assertEquals(3, Upgrader.numberOfApplicationsToUpgrade(30_000, 120_000, 7.5));
-
- // Run upgrades for 20 minutes.
- int upgrades = 0;
- for (int i = 0, now = 0; i < 30; i++, now += 40_000)
- upgrades += Upgrader.numberOfApplicationsToUpgrade(40_000, now, 8.7);
- assertEquals(174, upgrades);
- }
-
- @Test
- void testUpgradeShuffling() {
- // Deploy applications on initial version
- var default0 = createAndDeploy("default0", "default");
- var default1 = createAndDeploy("default1", "default");
- var default2 = createAndDeploy("default2", "default");
- var applications = Map.of(default0.instanceId(), default0,
- default1.instanceId(), default1,
- default2.instanceId(), default2);
-
- // Throttle upgrades per run
- Upgrader upgrader = new Upgrader(tester.controller(),
- Duration.ofMinutes(10),
- new Random(1589787107000L)); // Fixed random seed
- upgrader.setUpgradesPerMinute(0.1);
-
- // Trigger some upgrades
- List<Version> versions = List.of(Version.fromString("6.2"), Version.fromString("6.3"));
- Set<List<ApplicationId>> upgradeOrders = new HashSet<>(versions.size());
- for (var version : versions) {
- // Upgrade system
- tester.controllerTester().upgradeSystem(version);
- List<ApplicationId> upgraderOrder = new ArrayList<>(applications.size());
-
- // Upgrade all applications
- for (int i = 0; i < applications.size(); i++) {
- upgrader.maintain();
- tester.triggerJobs();
- Set<ApplicationId> triggered = tester.jobs().active().stream()
- .map(Run::id)
- .map(RunId::application)
- .collect(Collectors.toSet());
- assertEquals(1, triggered.size(), "Expected number of applications is triggered");
- ApplicationId application = triggered.iterator().next();
- upgraderOrder.add(application);
- applications.get(application).completeRollout();
- tester.clock().advance(Duration.ofMinutes(1));
- }
- upgradeOrders.add(upgraderOrder);
- }
- assertEquals(versions.size(), upgradeOrders.size(), "Upgrade orders are distinct");
- }
-
- private static final ApplicationPackage canaryApplicationPackage =
- new ApplicationPackageBuilder().upgradePolicy("canary")
- .region("us-west-1")
- .region("us-east-3")
- .build();
-
- private static final ApplicationPackage defaultApplicationPackage =
- new ApplicationPackageBuilder().upgradePolicy("default")
- .region("us-west-1")
- .region("us-east-3")
- .build();
-
- private static final ApplicationPackage conservativeApplicationPackage =
- new ApplicationPackageBuilder().upgradePolicy("conservative")
- .region("us-west-1")
- .region("us-east-3")
- .build();
-
- /** Returns empty prebuilt applications for efficiency */
- private ApplicationPackage applicationPackage(String upgradePolicy) {
- switch (upgradePolicy) {
- case "canary" : return canaryApplicationPackage;
- case "default" : return defaultApplicationPackage;
- case "conservative" : return conservativeApplicationPackage;
- default : throw new IllegalArgumentException("No upgrade policy '" + upgradePolicy + "'");
- }
- }
-
- private DeploymentContext createAndDeploy(String applicationName, String upgradePolicy) {
- return tester.newDeploymentContext("tenant1", applicationName, "default")
- .submit(applicationPackage(upgradePolicy))
- .deploy();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainerTest.java
deleted file mode 100644
index 4a49638bfc2..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/UserManagementMaintainerTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author olaa
- */
-public class UserManagementMaintainerTest {
-
- private final String TENANT_1 = "tenant1";
- private final String TENANT_2 = "tenant2";
- private final String APP_NAME = "some-app";
-
- @Test
- void deletes_tenant_when_not_public() {
- var tester = createTester(SystemName.main);
- var maintainer = new UserManagementMaintainer(tester.controller(), Duration.ofMinutes(5), tester.serviceRegistry().roleMaintainer());
- maintainer.maintain();
-
- var tenants = tester.controller().tenants().asList();
- var apps = tester.controller().applications().asList();
- assertEquals(1, tenants.size());
- assertEquals(1, apps.size());
- assertEquals(TENANT_2, tenants.get(0).name().value());
- }
-
- @Test
- void no_tenant_deletion_in_public() {
- var tester = createTester(SystemName.Public);
- var maintainer = new UserManagementMaintainer(tester.controller(), Duration.ofMinutes(5), tester.serviceRegistry().roleMaintainer());
- maintainer.maintain();
-
- var tenants = tester.controller().tenants().asList();
- var apps = tester.controller().applications().asList();
- assertEquals(2, tenants.size());
- assertEquals(2, apps.size());
- }
-
- private ControllerTester createTester(SystemName systemName) {
- var tester = new ControllerTester(systemName);
- tester.createTenant(TENANT_1);
- tester.createTenant(TENANT_2);
- tester.createApplication(TENANT_1, APP_NAME);
- tester.createApplication(TENANT_2, APP_NAME);
-
- var tenantToDelete = tester.controller().tenants().get(TENANT_1).get();
- tester.serviceRegistry().roleMaintainerMock().mockTenantToDelete(tenantToDelete);
- return tester;
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainerTest.java
deleted file mode 100644
index 6ffaf5fe0b6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VcmrMaintainerTest.java
+++ /dev/null
@@ -1,320 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction.State;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VcmrReport;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest.Status;
-import com.yahoo.vespa.hosted.controller.integration.MetricsMock;
-import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.time.DayOfWeek;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.ZonedDateTime;
-import java.time.temporal.TemporalAdjusters;
-import java.util.List;
-
-import static com.yahoo.vespa.hosted.controller.maintenance.VcmrMaintainer.TRACKED_CMRS_METRIC;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author olaa
- */
-public class VcmrMaintainerTest {
-
- private ControllerTester tester;
- private VcmrMaintainer maintainer;
- private NodeRepositoryMock nodeRepo;
- private MetricsMock metrics;
- private final ZoneId zoneId = ZoneId.from("prod.us-east-3");
- private final ZoneId zone2 = ZoneId.from("prod.us-west-1");
- private final HostName host1 = HostName.of("host1");
- private final HostName host2 = HostName.of("host2");
- private final HostName host3 = HostName.of("host3");
- private final String changeRequestId = "id123";
-
- @BeforeEach
- public void setup() {
- tester = new ControllerTester();
- metrics = new MetricsMock();
- maintainer = new VcmrMaintainer(tester.controller(), Duration.ofMinutes(1), metrics);
- nodeRepo = tester.serviceRegistry().configServer().nodeRepository().allowPatching(true);
- }
-
- @Test
- void recycle_hosts_after_completion() {
- var vcmrReport = new VcmrReport();
- vcmrReport.addVcmr(new ChangeRequestSource("aws", "id123", "url", ChangeRequestSource.Status.WAITING_FOR_APPROVAL , ZonedDateTime.now(), ZonedDateTime.now(), "N/A"));
- var parkedNode = createNode(host1, NodeType.host, Node.State.parked, true);
- var failedNode = createNode(host2, NodeType.host, Node.State.failed, false);
- var reports = vcmrReport.toNodeReports();
- parkedNode = Node.builder(parkedNode)
- .reports(reports)
- .build();
-
- nodeRepo.putNodes(zoneId, List.of(parkedNode, failedNode));
-
- tester.curator().writeChangeRequest(canceledChangeRequest());
- maintainer.maintain();
-
- // Only the parked node is recycled, VCMR report is cleared
- var nodeList = nodeRepo.list(zoneId, NodeFilter.all().hostnames(host1, host2));
- assertEquals(Node.State.dirty, nodeList.get(0).state());
- assertEquals(Node.State.failed, nodeList.get(1).state());
-
- assertTrue(nodeList.get(0).reports().isEmpty());
-
- var writtenChangeRequest = tester.curator().readChangeRequest(changeRequestId).get();
- assertEquals(Status.COMPLETED, writtenChangeRequest.getStatus());
- }
-
- @Test
- void infrastructure_hosts_require_maunal_intervention() {
- var configNode = createNode(host1, NodeType.config, Node.State.active, false);
- var activeNode = createNode(host2, NodeType.host, Node.State.active, false);
- nodeRepo.putNodes(zoneId, List.of(configNode, activeNode));
- nodeRepo.hasSpareCapacity(true);
-
- tester.curator().writeChangeRequest(futureChangeRequest());
- maintainer.maintain();
-
- var writtenChangeRequest = tester.curator().readChangeRequest(changeRequestId).get();
- var actionPlan = writtenChangeRequest.getHostActionPlan();
- assertEquals(2, actionPlan.size());
- var configAction = findHostAction(actionPlan, configNode);
- var tenantHostAction = findHostAction(actionPlan, activeNode);
- assertEquals(State.REQUIRES_OPERATOR_ACTION, configAction.getState());
- assertEquals(State.PENDING_RETIREMENT, tenantHostAction.getState());
- assertEquals(Status.REQUIRES_OPERATOR_ACTION, writtenChangeRequest.getStatus());
- }
-
- @Test
- void retires_hosts_when_near_vcmr() {
- var activeNode = createNode(host1, NodeType.host, Node.State.active, false);
- var failedNode = createNode(host2, NodeType.host, Node.State.failed, false);
- nodeRepo.putNodes(zoneId, List.of(activeNode, failedNode));
- nodeRepo.hasSpareCapacity(true);
-
- tester.curator().writeChangeRequest(startingChangeRequest());
- maintainer.maintain();
-
- var writtenChangeRequest = tester.curator().readChangeRequest(changeRequestId).orElseThrow();
- var actionPlan = writtenChangeRequest.getHostActionPlan();
- var parkedNodeAction = findHostAction(actionPlan, activeNode);
- var failedNodeAction = findHostAction(actionPlan, failedNode);
- assertEquals(State.RETIRING, parkedNodeAction.getState());
- assertEquals(State.NONE, failedNodeAction.getState());
- assertEquals(Status.IN_PROGRESS, writtenChangeRequest.getStatus());
-
- activeNode = nodeRepo.list(zoneId, NodeFilter.all().hostnames(activeNode.hostname())).get(0);
- assertTrue(activeNode.wantToRetire());
- }
-
- @Test
- void no_spare_capacity_requires_operator_action() {
- var activeNode = createNode(host1, NodeType.host, Node.State.active, false);
- var failedNode = createNode(host2, NodeType.host, Node.State.failed, false);
- nodeRepo.putNodes(zoneId, List.of(activeNode, failedNode));
- nodeRepo.hasSpareCapacity(false);
-
- tester.curator().writeChangeRequest(startingChangeRequest());
- maintainer.maintain();
-
- var writtenChangeRequest = tester.curator().readChangeRequest(changeRequestId).orElseThrow();
- var actionPlan = writtenChangeRequest.getHostActionPlan();
- var activeNodeAction = findHostAction(actionPlan, activeNode);
- var failedNodeAction = findHostAction(actionPlan, failedNode);
- assertEquals(State.REQUIRES_OPERATOR_ACTION, activeNodeAction.getState());
- assertEquals(State.REQUIRES_OPERATOR_ACTION, failedNodeAction.getState());
- assertEquals(Status.REQUIRES_OPERATOR_ACTION, writtenChangeRequest.getStatus());
-
- var approvedChangeRequests = tester.serviceRegistry().changeRequestClient().getApprovedChangeRequests();
- assertTrue(approvedChangeRequests.isEmpty());
- }
-
- @Test
- void updates_status_when_retiring_host_is_parked() {
- var parkedNode = createNode(host1, NodeType.host, Node.State.parked, true);
- nodeRepo.putNodes(zoneId, parkedNode);
- nodeRepo.hasSpareCapacity(true);
-
- tester.curator().writeChangeRequest(inProgressChangeRequest());
- maintainer.maintain();
-
- var writtenChangeRequest = tester.curator().readChangeRequest(changeRequestId).orElseThrow();
- var parkedNodeAction = writtenChangeRequest.getHostActionPlan().get(0);
- assertEquals(State.RETIRED, parkedNodeAction.getState());
- assertEquals(Status.READY, writtenChangeRequest.getStatus());
- }
-
- @Test
- void pending_retirement_when_vcmr_is_far_ahead() {
- var activeNode = createNode(host2, NodeType.host, Node.State.active, false);
- nodeRepo.putNodes(zoneId, List.of(activeNode));
- nodeRepo.hasSpareCapacity(true);
-
- tester.curator().writeChangeRequest(futureChangeRequest());
- maintainer.maintain();
-
- var writtenChangeRequest = tester.curator().readChangeRequest(changeRequestId).get();
- var tenantHostAction = writtenChangeRequest.getHostActionPlan().get(0);
- assertEquals(State.PENDING_RETIREMENT, tenantHostAction.getState());
- assertEquals(Status.PENDING_ACTION, writtenChangeRequest.getStatus());
-
- var approvedChangeRequests = tester.serviceRegistry().changeRequestClient().getApprovedChangeRequests();
- assertEquals(1, approvedChangeRequests.size());
-
- activeNode = nodeRepo.list(zoneId, NodeFilter.all().hostnames(host2)).get(0);
- var report = VcmrReport.fromReports(activeNode.reports());
- var reportAdded = report.getVcmrs().stream()
- .filter(vcmr -> vcmr.id().equals(changeRequestId))
- .count() == 1;
- assertTrue(reportAdded);
- }
-
- @Test
- void recycles_nodes_if_vcmr_is_postponed() {
- var parkedNode = createNode(host1, NodeType.host, Node.State.parked, false);
- var retiringNode = createNode(host2, NodeType.host, Node.State.active, true);
- nodeRepo.putNodes(zoneId, List.of(parkedNode, retiringNode));
- nodeRepo.hasSpareCapacity(true);
-
- tester.curator().writeChangeRequest(postponedChangeRequest());
- maintainer.maintain();
-
- var writtenChangeRequest = tester.curator().readChangeRequest(changeRequestId).get();
- var hostAction = writtenChangeRequest.getHostActionPlan().get(0);
- assertEquals(State.PENDING_RETIREMENT, hostAction.getState());
-
- parkedNode = nodeRepo.list(zoneId, NodeFilter.all().hostnames(parkedNode.hostname())).get(0);
- assertEquals(Node.State.dirty, parkedNode.state());
- assertFalse(parkedNode.wantToRetire());
-
- retiringNode = nodeRepo.list(zoneId, NodeFilter.all().hostnames(retiringNode.hostname())).get(0);
- assertEquals(Node.State.active, retiringNode.state());
- assertFalse(retiringNode.wantToRetire());
- }
-
- @Test
- void handle_multizone_vcmr() {
- var configNode = createNode(host1, NodeType.config, Node.State.active, false);
- var tenantNode1 = createNode(host2, NodeType.host, Node.State.active, false);
- var tenantNode2 = createNode(host3, NodeType.host, Node.State.active, false);
- nodeRepo.putNodes(zoneId, List.of(configNode, tenantNode1));
- nodeRepo.putNodes(zone2, List.of(tenantNode2));
- nodeRepo.hasSpareCapacity(true);
-
- tester.curator().writeChangeRequest(futureChangeRequest());
- maintainer.maintain();
-
- var writtenChangeRequest = tester.curator().readChangeRequest(changeRequestId).get();
- var actionPlan = writtenChangeRequest.getHostActionPlan();
-
- var configAction = findHostAction(actionPlan, configNode);
- var tenantAction1 = findHostAction(actionPlan, tenantNode1);
- var tenantAction2 = findHostAction(actionPlan, tenantNode2);
-
- assertEquals(State.REQUIRES_OPERATOR_ACTION, configAction.getState());
- assertEquals(State.PENDING_RETIREMENT, tenantAction1.getState());
- assertEquals(State.PENDING_RETIREMENT, tenantAction2.getState());
- }
-
- @Test
- void out_of_sync_when_manual_reactivation() {
- var nonRetiringNode = createNode(host1, NodeType.host, Node.State.active, false);
- nodeRepo.putNodes(zoneId, nonRetiringNode);
-
- tester.curator().writeChangeRequest(inProgressChangeRequest());
- maintainer.maintain();
-
- var writtenChangeRequest = tester.curator().readChangeRequest(changeRequestId).get();
- var actionPlan = writtenChangeRequest.getHostActionPlan();
-
- var action = findHostAction(actionPlan, nonRetiringNode);
-
- assertEquals(State.OUT_OF_SYNC, action.getState());
- assertEquals(Status.OUT_OF_SYNC, writtenChangeRequest.getStatus());
- assertEquals(1, metrics.getMetric(context -> "OUT_OF_SYNC".equals(context.get("status")), TRACKED_CMRS_METRIC).get());
- assertEquals(0, metrics.getMetric(context -> "REQUIRES_OPERATOR_ACTION".equals(context.get("status")), TRACKED_CMRS_METRIC).get());
- }
-
- @Test
- void retirement_start_time_ignores_weekends() {
- var plannedStartTime = ZonedDateTime.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.WEDNESDAY));
- var retirementStartTime = maintainer.getRetirementStartTime(plannedStartTime);
- assertEquals(plannedStartTime.minusDays(2), retirementStartTime);
-
- plannedStartTime = ZonedDateTime.now().with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY));
- retirementStartTime = maintainer.getRetirementStartTime(plannedStartTime);
- assertEquals(plannedStartTime.minusDays(4), retirementStartTime);
- }
-
- private VespaChangeRequest canceledChangeRequest() {
- return newChangeRequest(ChangeRequestSource.Status.CANCELED, State.RETIRED, State.RETIRING, ZonedDateTime.now());
- }
-
- private VespaChangeRequest futureChangeRequest() {
- return newChangeRequest(ChangeRequestSource.Status.WAITING_FOR_APPROVAL, State.NONE, State.NONE, ZonedDateTime.now().plus(Duration.ofDays(5L)));
- }
-
- private VespaChangeRequest startingChangeRequest() {
- return newChangeRequest(ChangeRequestSource.Status.STARTED, State.PENDING_RETIREMENT, State.NONE, ZonedDateTime.now());
- }
-
- private VespaChangeRequest inProgressChangeRequest() {
- return newChangeRequest(ChangeRequestSource.Status.STARTED, State.RETIRING, State.RETIRING, ZonedDateTime.now());
- }
-
- private VespaChangeRequest postponedChangeRequest() {
- return newChangeRequest(ChangeRequestSource.Status.STARTED, State.RETIRED, State.RETIRING, ZonedDateTime.now().plus(Duration.ofDays(8)));
- }
-
-
- private VespaChangeRequest newChangeRequest(ChangeRequestSource.Status sourceStatus, State state1, State state2, ZonedDateTime startTime) {
- var source = new ChangeRequestSource("aws", changeRequestId, "url", sourceStatus , startTime, ZonedDateTime.now(), "N/A");
- var actionPlan = List.of(
- new HostAction(host1.value(), state1, Instant.now()),
- new HostAction(host2.value(), state2, Instant.now())
- );
- return new VespaChangeRequest(
- changeRequestId,
- source,
- List.of("switch1"),
- List.of("host1", "host2", "host3"),
- ChangeRequest.Approval.REQUESTED,
- ChangeRequest.Impact.VERY_HIGH,
- VespaChangeRequest.Status.IN_PROGRESS,
- actionPlan,
- ZoneId.from("prod.us-east-3")
- );
- }
-
- private Node createNode(HostName hostname, NodeType nodeType, Node.State state, boolean wantToRetire) {
- return Node.builder()
- .hostname(hostname)
- .type(nodeType)
- .state(state)
- .wantToRetire(wantToRetire)
- .build();
- }
-
- private HostAction findHostAction(List<HostAction> actions, Node node) {
- return actions.stream()
- .filter(action -> node.hostname().value().equals(action.getHostname()))
- .findFirst().orElseThrow();
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdaterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdaterTest.java
deleted file mode 100644
index 6e6013b070e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/VersionStatusUpdaterTest.java
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.maintenance;
-
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.SystemMonitor;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.util.Collections;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author bratseth
- */
-public class VersionStatusUpdaterTest {
-
- /** Test that this job updates the status. Test of the content of the update is in
- * {@link com.yahoo.vespa.hosted.controller.versions.VersionStatusTest} */
- @Test
- void testVersionUpdating() {
- ControllerTester tester = new ControllerTester();
- tester.controller().updateVersionStatus(VersionStatus.empty());
- assertFalse(tester.controller().readVersionStatus().systemVersion().isPresent());
-
- VersionStatusUpdater updater = new VersionStatusUpdater(tester.controller(), Duration.ofDays(1));
- updater.maintain();
- assertTrue(tester.controller().readVersionStatus().systemVersion().isPresent());
- }
-
- @Test
- void testConfidenceConversion() {
- List.of(VespaVersion.Confidence.values()).forEach(VersionStatusUpdater::convert);
- assertEquals(SystemMonitor.Confidence.broken, VersionStatusUpdater.convert(VespaVersion.Confidence.broken));
- assertEquals(SystemMonitor.Confidence.low, VersionStatusUpdater.convert(VespaVersion.Confidence.low));
- assertEquals(SystemMonitor.Confidence.normal, VersionStatusUpdater.convert(VespaVersion.Confidence.normal));
- assertEquals(SystemMonitor.Confidence.high, VersionStatusUpdater.convert(VespaVersion.Confidence.high));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationFormatterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationFormatterTest.java
deleted file mode 100644
index 875487144d9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationFormatterTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.time.Instant;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author enygaard
- */
-public class NotificationFormatterTest {
- private final TenantName tenant = TenantName.from("scoober");
- private final ApplicationName application = ApplicationName.from("myapp");
- private final InstanceName instance = InstanceName.from("beta");
- private final ApplicationId applicationId = ApplicationId.from(tenant, application, instance);
- private final DeploymentId deploymentId = new DeploymentId(applicationId, ZoneId.defaultId());
- private final ClusterSpec.Id cluster = new ClusterSpec.Id("content");
-
- private final NotificationFormatter formatter = new NotificationFormatter(new ConsoleUrls(URI.create("https://console.tld")));
-
- @Test
- void applicationPackage() {
- var notification = new Notification(Instant.now(), Notification.Type.applicationPackage, Notification.Level.warning, NotificationSource.from(applicationId), List.of("1", "2"));
- var content = formatter.format(notification);
- assertEquals("Application package", content.prettyType());
- assertEquals("Application package for myapp.beta has 2 warnings", content.messagePrefix());
- assertEquals("https://console.tld/tenant/scoober/application/myapp/prod/instance/beta", content.uri());
- }
-
- @Test
- void deployment() {
- var runId = new RunId(applicationId, JobType.prod(RegionName.defaultName()), 1001);
- var notification = new Notification(Instant.now(), Notification.Type.deployment, Notification.Level.warning, NotificationSource.from(runId), List.of("1"));
- var content = formatter.format(notification);
- assertEquals("Deployment", content.prettyType());
- assertEquals("production-default #1001 for myapp.beta has a warning", content.messagePrefix());
- assertEquals("https://console.tld/tenant/scoober/application/myapp/prod/instance/beta/job/production-default/run/1001", content.uri());
- }
-
- @Test
- void deploymentError() {
- var runId = new RunId(applicationId, JobType.prod(RegionName.defaultName()), 1001);
- var notification = new Notification(Instant.now(), Notification.Type.deployment, Notification.Level.error, NotificationSource.from(runId), List.of("1"));
- var content = formatter.format(notification);
- assertEquals("Deployment", content.prettyType());
- assertEquals("production-default #1001 for myapp.beta has failed", content.messagePrefix());
- assertEquals("https://console.tld/tenant/scoober/application/myapp/prod/instance/beta/job/production-default/run/1001", content.uri());
- }
-
- @Test
- void testPackage() {
- var notification = new Notification(Instant.now(), Notification.Type.testPackage, Notification.Level.warning, NotificationSource.from(TenantAndApplicationId.from(applicationId)), List.of("1"));
- var content = formatter.format(notification);
- assertEquals("Test package", content.prettyType());
- assertEquals("There is a problem with tests for myapp", content.messagePrefix());
- assertEquals("https://console.tld/tenant/scoober/application/myapp/prod/instance", content.uri());
- }
-
- @Test
- void reindex() {
- var notification = new Notification(Instant.now(), Notification.Type.reindex, Notification.Level.info, NotificationSource.from(deploymentId, cluster), List.of("1"));
- var content = formatter.format(notification);
- assertEquals("Reindex", content.prettyType());
- assertEquals("Cluster content in prod.default for myapp.beta is reindexing", content.messagePrefix());
- assertEquals("https://console.tld/tenant/scoober/application/myapp/prod/instance/beta?beta.prod.default=clusters%2Ccontent%3Dreindexing", content.uri());
- }
-
- @Test
- void feedBlock() {
- var notification = new Notification(Instant.now(), Notification.Type.feedBlock, Notification.Level.warning, NotificationSource.from(deploymentId, cluster), List.of("1"));
- var content = formatter.format(notification);
- assertEquals("Nearly feed blocked", content.prettyType());
- assertEquals("Cluster content in prod.default for myapp.beta is nearly feed blocked", content.messagePrefix());
- assertEquals("https://console.tld/tenant/scoober/application/myapp/prod/instance/beta?beta.prod.default=clusters%2Ccontent", content.uri());
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java
deleted file mode 100644
index e41be11c846..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotificationsDbTest.java
+++ /dev/null
@@ -1,322 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-import com.google.common.collect.ImmutableBiMap;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.path.Path;
-import com.yahoo.test.ManualClock;
-import com.yahoo.vespa.flags.FlagSource;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.ClusterMetrics;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock;
-import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Email;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Collectors;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.configserver.ApplicationReindexing.Cluster;
-import static com.yahoo.vespa.hosted.controller.notification.Notification.Level;
-import static com.yahoo.vespa.hosted.controller.notification.Notification.Type;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author freva
- */
-public class NotificationsDbTest {
-
- private static final ApplicationReindexing emptyReindexing = new ApplicationReindexing(false, Map.of());
- private static final TenantName tenant = TenantName.from("tenant1");
- private static final Email email = new Email("user1@example.com", true);
- private static final CloudTenant cloudTenant = new CloudTenant(tenant,
- Instant.now(),
- LastLoginInfo.EMPTY,
- Optional.empty(),
- ImmutableBiMap.of(),
- TenantInfo.empty()
- .withContacts(new TenantContacts(
- List.of(new TenantContacts.EmailContact(
- List.of(TenantContacts.Audience.NOTIFICATIONS),
- email)))),
- List.of(),
- new ArchiveAccess(),
- Optional.empty(),
- Instant.EPOCH,
- List.of(),
- Optional.empty(),
- PlanId.from("none"));
- private static final List<Notification> notifications = List.of(
- notification(1001, Type.deployment, Level.error, NotificationSource.from(tenant), "tenant msg"),
- notification(1101, Type.applicationPackage, Level.warning, NotificationSource.from(TenantAndApplicationId.from(tenant.value(), "app1")), "app msg"),
- notification(1201, Type.deployment, Level.error, NotificationSource.from(ApplicationId.from(tenant.value(), "app2", "instance2")), "instance msg"),
- notification(1301, Type.deployment, Level.warning, NotificationSource.from(new DeploymentId(ApplicationId.from(tenant.value(), "app2", "instance2"), ZoneId.from("prod", "us-north-2"))), "deployment msg"),
- notification(1401, Type.feedBlock, Level.error, NotificationSource.from(new DeploymentId(ApplicationId.from(tenant.value(), "app1", "instance1"), ZoneId.from("dev", "us-south-1")), ClusterSpec.Id.from("cluster1")), "cluster msg"),
- notification(1501, Type.deployment, Level.warning, NotificationSource.from(new RunId(ApplicationId.from(tenant.value(), "app1", "instance1"), DeploymentContext.devUsEast1, 4)), "run id msg"));
-
- private final ManualClock clock = new ManualClock(Instant.ofEpochSecond(12345));
- private final MockCuratorDb curatorDb = new MockCuratorDb(SystemName.Public);
- private final MockMailer mailer = new MockMailer();
- private final FlagSource flagSource = new InMemoryFlagSource().withBooleanFlag(PermanentFlags.NOTIFICATION_DISPATCH_FLAG.id(), true);
- private final ConsoleUrls consoleUrls = new ConsoleUrls(URI.create("https://console.tld"));
- private final NotificationsDb notificationsDb = new NotificationsDb(clock, curatorDb, new Notifier(curatorDb, consoleUrls, mailer, flagSource), consoleUrls);
-
- @Test
- void list_test() {
- assertEquals(notifications, notificationsDb.listNotifications(NotificationSource.from(tenant), false));
- assertEquals(notificationIndices(0, 1, 2, 3), notificationsDb.listNotifications(NotificationSource.from(tenant), true));
- assertEquals(notificationIndices(2, 3), notificationsDb.listNotifications(NotificationSource.from(TenantAndApplicationId.from(tenant.value(), "app2")), false));
- assertEquals(notificationIndices(4, 5), notificationsDb.listNotifications(NotificationSource.from(ApplicationId.from(tenant.value(), "app1", "instance1")), false));
- assertEquals(notificationIndices(5), notificationsDb.listNotifications(NotificationSource.from(new RunId(ApplicationId.from(tenant.value(), "app1", "instance1"), DeploymentContext.devUsEast1, 5)), false));
- assertEquals(List.of(), notificationsDb.listNotifications(NotificationSource.from(new RunId(ApplicationId.from(tenant.value(), "app1", "instance1"), DeploymentContext.productionUsEast3, 4)), false));
- }
-
- @Test
- void add_test() {
- Notification notification1 = notification(12345, Type.deployment, Level.warning, NotificationSource.from(ApplicationId.from(tenant.value(), "app2", "instance2")), "instance msg #2");
- Notification notification2 = notification(12345, Type.deployment, Level.error, NotificationSource.from(ApplicationId.from(tenant.value(), "app3", "instance2")), "instance msg #3");
-
- // Replace the 3rd notification
- setNotification(notification1);
-
- // Notification for a new app, add without replacement
- setNotification(notification2);
-
- List<Notification> expected = notificationIndices(0, 1, 3, 4, 5);
- expected.addAll(List.of(notification1, notification2));
- assertEquals(expected, curatorDb.readNotifications(tenant));
- }
-
- @Test
- void notifier_test() {
- Notification notification1 = notification(12345, Type.deployment, Level.warning, NotificationSource.from(ApplicationId.from(tenant.value(), "app2", "instance2")), "instance msg #2");
- Notification notification2 = notification(12345, Type.applicationPackage, Level.error, NotificationSource.from(ApplicationId.from(tenant.value(), "app3", "instance2")), "instance msg #3");
- Notification notification3 = notification(12345, Type.reindex, Level.warning, NotificationSource.from(new DeploymentId(ApplicationId.from(tenant.value(), "app2", "instance2"), ZoneId.defaultId()), new ClusterSpec.Id("content")), "instance msg #2");
- ;
- var a = notifications.get(0);
- setNotification(a);
- assertEquals(0, mailer.inbox(email.getEmailAddress()).size());
-
- // Replace the 3rd notification. but don't change source or type
- setNotification(notification1);
- assertEquals(0, mailer.inbox(email.getEmailAddress()).size());
-
- // Notification for a new app, add without replacement
- setNotification(notification2);
- assertEquals(1, mailer.inbox(email.getEmailAddress()).size());
-
- // Notification for new type on existing app
- setNotification(notification3);
- assertEquals(2, mailer.inbox(email.getEmailAddress()).size());
- }
-
- @Test
- void remove_single_test() {
- // Remove the 3rd notification
- notificationsDb.removeNotification(NotificationSource.from(ApplicationId.from(tenant.value(), "app2", "instance2")), Type.deployment);
-
- // Removing something that doesn't exist is OK
- notificationsDb.removeNotification(NotificationSource.from(ApplicationId.from(tenant.value(), "app3", "instance2")), Type.deployment);
-
- assertEquals(notificationIndices(0, 1, 3, 4, 5), curatorDb.readNotifications(tenant));
- }
-
- @Test
- void remove_multiple_test() {
- // Remove the 3rd notification
- notificationsDb.removeNotifications(NotificationSource.from(ApplicationId.from(tenant.value(), "app1", "instance1")));
- assertEquals(notificationIndices(0, 1, 2, 3), curatorDb.readNotifications(tenant));
- assertTrue(curatorDb.curator().exists(Path.fromString("/controller/v1/notifications/" + tenant.value())));
-
- notificationsDb.removeNotifications(NotificationSource.from(tenant));
- assertEquals(List.of(), curatorDb.readNotifications(tenant));
- assertFalse(curatorDb.curator().exists(Path.fromString("/controller/v1/notifications/" + tenant.value())));
- }
-
- @Test
- void deployment_metrics_notify_test() {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenant.value(), "app1", "instance1"), ZoneId.from("prod", "us-south-3"));
-
- // No metrics, no new notification
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(), emptyReindexing);
- assertEquals(0, mailer.inbox(email.getEmailAddress()).size());
-
- // Metrics that contain none of the feed block metrics does not create new notification
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(clusterMetrics("cluster1", null, null, null, null)), emptyReindexing);
- assertEquals(0, mailer.inbox(email.getEmailAddress()).size());
-
- // One resource is at warning
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(clusterMetrics("cluster1", 0.88, 0.9, 0.3, 0.5)), emptyReindexing);
- assertEquals(1, mailer.inbox(email.getEmailAddress()).size());
-
- // One resource over the limit
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(clusterMetrics("cluster1", 0.95, 0.9, 0.3, 0.5)), emptyReindexing);
- assertEquals(2, mailer.inbox(email.getEmailAddress()).size());
- }
-
- @Test
- void feed_blocked_single_cluster_test() {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenant.value(), "app1", "instance1"), ZoneId.from("prod", "us-south-3"));
- NotificationSource sourceCluster1 = NotificationSource.from(deploymentId, ClusterSpec.Id.from("cluster1"));
- List<Notification> expected = new ArrayList<>(notifications);
-
- // No metrics, no new notification
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(), emptyReindexing);
- assertEquals(expected, curatorDb.readNotifications(tenant));
-
- // Metrics that contain none of the feed block metrics does not create new notification
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(clusterMetrics("cluster1", null, null, null, null)), emptyReindexing);
- assertEquals(expected, curatorDb.readNotifications(tenant));
-
- // Metrics that only contain util or limit (should not be possible) should not cause any issues
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(clusterMetrics("cluster1", 0.95, null, null, 0.5)), emptyReindexing);
- assertEquals(expected, curatorDb.readNotifications(tenant));
-
- // One resource is at warning
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(clusterMetrics("cluster1", 0.88, 0.9, 0.3, 0.5)), emptyReindexing);
- expected.add(notification(12345, Type.feedBlock, Level.warning, "Cluster [cluster1](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1?instance1.prod.us-south-3=clusters%2Ccluster1) in **prod.us-south-3** for **app1.instance1** is nearly feed blocked",
- sourceCluster1, "disk (usage: 88.0%, feed block limit: 90.0%)"));
- assertEquals(expected, curatorDb.readNotifications(tenant));
-
- // Both resources over the limit
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(clusterMetrics("cluster1", 0.95, 0.9, 0.3, 0.5)), emptyReindexing);
- expected.set(6, notification(12345, Type.feedBlock, Level.error, "Cluster [cluster1](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1?instance1.prod.us-south-3=clusters%2Ccluster1) in **prod.us-south-3** for **app1.instance1** is feed blocked",
- sourceCluster1, "disk (usage: 95.0%, feed block limit: 90.0%)"));
- assertEquals(expected, curatorDb.readNotifications(tenant));
-
- // One resource at warning, one at error: Only show error message
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(clusterMetrics("cluster1", 0.95, 0.9, 0.7, 0.5)), emptyReindexing);
- expected.set(6, notification(12345, Type.feedBlock, Level.error, "Cluster [cluster1](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1?instance1.prod.us-south-3=clusters%2Ccluster1) in **prod.us-south-3** for **app1.instance1** is feed blocked", sourceCluster1,
- "memory (usage: 70.0%, feed block limit: 50.0%)", "disk (usage: 95.0%, feed block limit: 90.0%)"));
- assertEquals(expected, curatorDb.readNotifications(tenant));
- }
-
- @Test
- void deployment_metrics_multiple_cluster_test() {
- DeploymentId deploymentId = new DeploymentId(ApplicationId.from(tenant.value(), "app1", "instance1"), ZoneId.from("prod", "us-south-3"));
- NotificationSource sourceCluster1 = NotificationSource.from(deploymentId, ClusterSpec.Id.from("cluster1"));
- NotificationSource sourceCluster2 = NotificationSource.from(deploymentId, ClusterSpec.Id.from("cluster2"));
- NotificationSource sourceCluster3 = NotificationSource.from(deploymentId, ClusterSpec.Id.from("cluster3"));
- List<Notification> expected = new ArrayList<>(notifications);
-
- // Cluster1 and cluster2 are having feed block issues, cluster 3 is reindexing
- ApplicationReindexing applicationReindexing1 = new ApplicationReindexing(true, Map.of(
- "cluster3", new Cluster(Map.of(), Map.of(
- "announcements", reindexingStatus("reindexing due to a schema change", 0.75),
- "build", reindexingStatus(null, 0.50)))));
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(
- clusterMetrics("cluster1", 0.88, 0.9, 0.3, 0.5), clusterMetrics("cluster2", 0.6, 0.8, 0.9, 0.75), clusterMetrics("cluster3", 0.1, 0.8, 0.2, 0.9)), applicationReindexing1);
- expected.add(notification(12345, Type.feedBlock, Level.warning, "Cluster [cluster1](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1?instance1.prod.us-south-3=clusters%2Ccluster1) in **prod.us-south-3** for **app1.instance1** is nearly feed blocked", sourceCluster1, "disk (usage: 88.0%, feed block limit: 90.0%)"));
- expected.add(notification(12345, Type.feedBlock, Level.error, "Cluster [cluster2](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1?instance1.prod.us-south-3=clusters%2Ccluster2) in **prod.us-south-3** for **app1.instance1** is feed blocked", sourceCluster2, "memory (usage: 90.0%, feed block limit: 75.0%)"));
- expected.add(notification(12345, Type.reindex, Level.info, "Cluster [cluster3](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1?instance1.prod.us-south-3=clusters%2Ccluster3%3Dreindexing) in **prod.us-south-3** for **app1.instance1** is [reindexing](https://docs.vespa.ai/en/operations/reindexing.html)", sourceCluster3, "document type 'announcements' reindexing due to a schema change (75.0% done)", "document type 'build' (50.0% done)"));
- assertEquals(expected, curatorDb.readNotifications(tenant));
-
- // Cluster1 improves, while cluster3 starts having feed block issues and finishes reindexing 'build' documents
- ApplicationReindexing applicationReindexing2 = new ApplicationReindexing(true, Map.of(
- "cluster3", new Cluster(Map.of(), Map.of(
- "announcements", reindexingStatus("reindexing due to a schema change", 0.90),
- "build", reindexingStatus(null, null)))));
- notificationsDb.setDeploymentMetricsNotifications(deploymentId, List.of(
- clusterMetrics("cluster1", 0.15, 0.9, 0.3, 0.5), clusterMetrics("cluster2", 0.6, 0.8, 0.9, 0.75), clusterMetrics("cluster3", 0.78, 0.8, 0.2, 0.9)), applicationReindexing2);
- expected.set(6, notification(12345, Type.feedBlock, Level.error, "Cluster [cluster2](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1?instance1.prod.us-south-3=clusters%2Ccluster2) in **prod.us-south-3** for **app1.instance1** is feed blocked", sourceCluster2, "memory (usage: 90.0%, feed block limit: 75.0%)"));
- expected.set(7, notification(12345, Type.feedBlock, Level.warning, "Cluster [cluster3](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1?instance1.prod.us-south-3=clusters%2Ccluster3) in **prod.us-south-3** for **app1.instance1** is nearly feed blocked", sourceCluster3, "disk (usage: 78.0%, feed block limit: 80.0%)"));
- expected.set(8, notification(12345, Type.reindex, Level.info, "Cluster [cluster3](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1?instance1.prod.us-south-3=clusters%2Ccluster3%3Dreindexing) in **prod.us-south-3** for **app1.instance1** is [reindexing](https://docs.vespa.ai/en/operations/reindexing.html)", sourceCluster3, "document type 'announcements' reindexing due to a schema change (90.0% done)"));
- assertEquals(expected, curatorDb.readNotifications(tenant));
- }
-
- @Test
- void title_test() {
- curatorDb.deleteNotifications(tenant);
- TenantAndApplicationId tenantApp = TenantAndApplicationId.from(tenant.value(), "app1");
- ApplicationId app = tenantApp.instance("instance1");
- ZoneRegistryMock zoneRegistry = new ZoneRegistryMock(SystemName.Public);
-
- notificationsDb.setApplicationPackageNotification(NotificationSource.from(tenantApp), List.of());
- notificationsDb.setApplicationPackageNotification(NotificationSource.from(new DeploymentId(app, ZoneId.from("dev.us-east-3"))), List.of());
- notificationsDb.setSubmissionNotification(tenantApp, "msg");
- notificationsDb.setTestPackageNotification(tenantApp, List.of());
- notificationsDb.setDeploymentNotification(new RunId(app, JobType.prod("us-east-3"), 123), "msg");
- notificationsDb.setDeploymentNotification(new RunId(app, JobType.productionTestOf(ZoneId.from("prod.us-east-3")), 123), "msg");
- notificationsDb.setDeploymentNotification(new RunId(app, JobType.systemTest(zoneRegistry, CloudName.AWS), 123), "msg");
- notificationsDb.setDeploymentNotification(new RunId(app, JobType.stagingTest(zoneRegistry, CloudName.AWS), 123), "msg");
- notificationsDb.setDeploymentNotification(new RunId(app, JobType.dev("us-east-3"), 123), "msg");
- assertEquals(List.of(
- "Application package for [app1](https://console.tld/tenant/tenant1/application/app1/prod/instance) has warnings",
- "Application package for [app1.instance1](https://console.tld/tenant/tenant1/application/app1/dev/instance/instance1) has warnings",
- "Application package for [app1](https://console.tld/tenant/tenant1/application/app1/prod/instance) has a warning",
- "There are problems with tests for [app1](https://console.tld/tenant/tenant1/application/app1/prod/instance)",
- "Deployment job [#123 to us-east-3](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1/job/production-us-east-3/run/123) for application **app1.instance1** has failed",
- "Test job [#123 to us-east-3](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1/job/test-us-east-3/run/123) for application **app1.instance1** has failed",
- "[System test #123](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1/job/system-test/run/123) for application **app1.instance1** has failed",
- "[Staging test #123](https://console.tld/tenant/tenant1/application/app1/prod/instance/instance1/job/staging-test/run/123) for application **app1.instance1** has failed",
- "Deployment job [#123 to dev.us-east-3](https://console.tld/tenant/tenant1/application/app1/dev/instance/instance1/job/dev-us-east-3/run/123) for application **app1.instance1** has failed"
- ), notificationsDb.listNotifications(NotificationSource.from(tenant), false).stream().map(Notification::title).toList());
- }
-
- @BeforeEach
- public void init() {
- curatorDb.writeNotifications(tenant, notifications);
- curatorDb.writeTenant(cloudTenant);
- mailer.reset();
- }
-
- private void setNotification(Notification notification) {
- notificationsDb.setNotification(notification.source(), notification.type(), notification.level(), "", notification.messages(), Optional.empty());
- }
-
- private static List<Notification> notificationIndices(int... indices) {
- return Arrays.stream(indices).mapToObj(notifications::get).collect(Collectors.toCollection(ArrayList::new));
- }
-
- private static Notification notification(long secondsSinceEpoch, Type type, Level level, NotificationSource source, String... messages) {
- return notification(secondsSinceEpoch, type, level, "", source, messages);
- }
-
- private static Notification notification(long secondsSinceEpoch, Type type, Level level, String title, NotificationSource source, String... messages) {
- return new Notification(Instant.ofEpochSecond(secondsSinceEpoch), type, level, source, title, List.of(messages));
- }
-
- private static ClusterMetrics clusterMetrics(String clusterId,
- Double diskUtil, Double diskLimit, Double memoryUtil, Double memoryLimit) {
- Map<String, Double> metrics = new HashMap<>();
- if (diskUtil != null) metrics.put(ClusterMetrics.DISK_UTIL, diskUtil);
- if (diskLimit != null) metrics.put(ClusterMetrics.DISK_FEED_BLOCK_LIMIT, diskLimit);
- if (memoryUtil != null) metrics.put(ClusterMetrics.MEMORY_UTIL, memoryUtil);
- if (memoryLimit != null) metrics.put(ClusterMetrics.MEMORY_FEED_BLOCK_LIMIT, memoryLimit);
- return new ClusterMetrics(clusterId, "content", metrics);
- }
-
- private static ApplicationReindexing.Status reindexingStatus(String causeOrNull, Double progressOrNull) {
- return new ApplicationReindexing.Status(null, null, null, null, null, progressOrNull, null, causeOrNull);
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java
deleted file mode 100644
index 55531dff72d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/notification/NotifierTest.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.notification;
-
-import com.google.common.collect.ImmutableBiMap;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.api.integration.ConsoleUrls;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMailer;
-import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Email;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.net.URI;
-import java.time.Instant;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class NotifierTest {
- private static final TenantName tenant = TenantName.from("tenant1");
- private static final Email email = new Email("user1@example.com", true);
-
- private static final CloudTenant cloudTenant = new CloudTenant(tenant,
- Instant.now(),
- LastLoginInfo.EMPTY,
- Optional.empty(),
- ImmutableBiMap.of(),
- TenantInfo.empty()
- .withContacts(new TenantContacts(
- List.of(new TenantContacts.EmailContact(
- List.of(TenantContacts.Audience.NOTIFICATIONS),
- email)))),
- List.of(),
- new ArchiveAccess(),
- Optional.empty(),
- Instant.EPOCH,
- List.of(),
- Optional.empty(),
- PlanId.from("none"));
-
-
- MockCuratorDb curatorDb = new MockCuratorDb(SystemName.Public);
-
- @BeforeEach
- public void init() {
- curatorDb.writeTenant(cloudTenant);
- }
-
- @Test
- void dispatch() throws IOException {
- var mailer = new MockMailer();
- var flagSource = new InMemoryFlagSource().withBooleanFlag(PermanentFlags.NOTIFICATION_DISPATCH_FLAG.id(), true);
- var notifier = new Notifier(curatorDb, new ConsoleUrls(URI.create("https://console.tld")), mailer, flagSource);
-
- var notification = new Notification(Instant.now(), Notification.Type.testPackage, Notification.Level.warning,
- NotificationSource.from(ApplicationId.from(tenant, ApplicationName.defaultName(), InstanceName.defaultName())),
- List.of("test package has production tests, but no production tests are declared in deployment.xml",
- "see https://docs.vespa.ai/en/testing.html for details on how to write system tests for Vespa"));
- notifier.dispatch(notification);
- assertEquals(1, mailer.inbox(email.getEmailAddress()).size());
- var mail = mailer.inbox(email.getEmailAddress()).get(0);
-
- assertEquals("[WARNING] Test package Vespa Notification for tenant1.default.default", mail.subject());
- assertEquals(new String(NotifierTest.class.getResourceAsStream("/mail/notification.html").readAllBytes()), mail.htmlMessage().get());
- }
-
- @Test
- void linkify() {
- var data = Map.of(
- "Hello. https://example.com/foo/bar.html is a nice place.", "Hello. <a href=\"https://example.com/foo/bar.html\">https://example.com/foo/bar.html</a> is a nice place.",
- "No url.", "No url.");
- data.forEach((input, expected) -> assertEquals(expected, Notifier.linkify(input)));
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
deleted file mode 100644
index 41362fd0b97..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ApplicationSerializerTest.java
+++ /dev/null
@@ -1,262 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationOverrides;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.SourceRevision;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.AccountId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.User;
-import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentActivity;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.QuotaUsage;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.RevisionHistory;
-import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationId;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationState;
-import com.yahoo.vespa.hosted.controller.routing.rotation.RotationStatus;
-import org.junit.jupiter.api.Test;
-
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.security.PublicKey;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.OptionalDouble;
-import java.util.OptionalInt;
-import java.util.OptionalLong;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author bratseth
- */
-
-public class ApplicationSerializerTest {
-
- private static final ApplicationSerializer APPLICATION_SERIALIZER = new ApplicationSerializer();
- private static final Path testData = Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/");
- private static final ZoneId zone1 = ZoneId.from("prod", "us-west-1");
- private static final ZoneId zone2 = ZoneId.from("prod", "us-east-3");
- private static final PublicKey publicKey = KeyUtils.fromPemEncodedPublicKey("""
- -----BEGIN PUBLIC KEY-----
- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9
- z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==
- -----END PUBLIC KEY-----
- """);
- private static final PublicKey otherPublicKey = KeyUtils.fromPemEncodedPublicKey("""
- -----BEGIN PUBLIC KEY-----
- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE
- pDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==
- -----END PUBLIC KEY-----
- """);
-
- @Test
- void testSerialization() throws Exception {
- DeploymentSpec deploymentSpec = DeploymentSpec.fromXml("""
- <deployment version='1.0'>
- <staging/>
- <instance id="i1">
- <prod>
- <region>us-west-1</region>
- </prod>
- </instance>
- </deployment>""");
- ValidationOverrides validationOverrides = ValidationOverrides.fromXml("<validation-overrides version='1.0'>" +
- " <allow until='2017-06-15'>deployment-removal</allow>" +
- "</validation-overrides>");
-
- OptionalLong projectId = OptionalLong.of(123L);
-
- ApplicationId id1 = ApplicationId.from("t1", "a1", "i1");
- ApplicationId id3 = ApplicationId.from("t1", "a1", "i3");
- List<Deployment> deployments = new ArrayList<>();
- ApplicationVersion applicationVersion1 = new ApplicationVersion(RevisionId.forProduction(31),
- Optional.of(new SourceRevision("git@github:org/repo.git", "branch1", "commit1")),
- Optional.of("william@shakespeare"),
- Optional.of(Version.fromString("1.2.3")),
- Optional.of(123),
- Optional.of(Instant.ofEpochMilli(666)),
- Optional.empty(),
- Optional.of("best commit"),
- Optional.of("hash1"),
- Optional.of(Instant.ofEpochMilli(777)),
- true,
- false,
- Optional.of("~(˘▾˘)~"),
- Optional.of(Instant.ofEpochMilli(496)),
- 3);
- assertEquals("https://github/org/repo/tree/commit1", applicationVersion1.sourceUrl().get());
-
- RevisionId id = RevisionId.forDevelopment(31, new JobId(id1, DeploymentContext.productionUsEast3));
- SourceRevision source = new SourceRevision("repo1", "branch1", "commit1");
- Version compileVersion = Version.fromString("6.3.1");
- ApplicationVersion applicationVersion2 = new ApplicationVersion(id, Optional.of(source), Optional.of("a@b"), Optional.of(compileVersion), Optional.empty(), Optional.of(Instant.ofEpochMilli(496)), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), true, false, Optional.empty(), Optional.empty(), 0);
- Instant activityAt = Instant.parse("2018-06-01T10:15:30.00Z");
- deployments.add(new Deployment(zone1, CloudAccount.empty, applicationVersion1.id(), Version.fromString("1.2.3"), Instant.ofEpochMilli(3),
- DeploymentMetrics.none, DeploymentActivity.none, QuotaUsage.none, OptionalDouble.empty(), Map.of(TokenId.of("foo"), Instant.ofEpochMilli(333))));
- deployments.add(new Deployment(zone2, CloudAccount.from("001122334455"), applicationVersion2.id(), Version.fromString("1.2.3"), Instant.ofEpochMilli(5),
- new DeploymentMetrics(2, 3, 4, 5, 6,
- Optional.of(Instant.now().truncatedTo(ChronoUnit.MILLIS)),
- Map.of(DeploymentMetrics.Warning.all, 3)),
- DeploymentActivity.create(Optional.of(activityAt), Optional.of(activityAt),
- OptionalDouble.of(200), OptionalDouble.of(10)),
- QuotaUsage.create(OptionalDouble.of(23.5)),
- OptionalDouble.of(12.3), Map.of()));
-
- var rotationStatus = RotationStatus.from(Map.of(new RotationId("my-rotation"),
- new RotationStatus.Targets(
- Map.of(ZoneId.from("prod", "us-west-1"), RotationState.in,
- ZoneId.from("prod", "us-east-3"), RotationState.out),
- Instant.ofEpochMilli(42))));
-
- RevisionHistory revisions = RevisionHistory.ofRevisions(List.of(applicationVersion1),
- Map.of(new JobId(id1, DeploymentContext.productionUsEast3), List.of(applicationVersion2)));
- List<Instance> instances =
- List.of(new Instance(id1,
- deployments,
- Map.of(DeploymentContext.systemTest, Instant.ofEpochMilli(333)),
- List.of(rotation("foo", "default", "my-rotation", Set.of("us-west-1"))),
- rotationStatus,
- Change.of(new Version("6.1"))),
- new Instance(id3,
- List.of(),
- Map.of(),
- List.of(),
- RotationStatus.EMPTY,
- Change.of(Version.fromString("6.7")).withPlatformPin().withRevisionPin()));
-
- Application original = new Application(TenantAndApplicationId.from(id1),
- Instant.now().truncatedTo(ChronoUnit.MILLIS),
- deploymentSpec,
- validationOverrides,
- Optional.of(IssueId.from("4321")),
- Optional.of(IssueId.from("1234")),
- Optional.of(User.from("by-username")),
- Optional.of(new AccountId("foo8ar")),
- OptionalInt.of(7),
- new ApplicationMetrics(0.5, 0.9),
- Set.of(publicKey, otherPublicKey),
- projectId,
- revisions,
- instances
- );
-
- Application serialized = APPLICATION_SERIALIZER.fromSlime(SlimeUtils.toJsonBytes(APPLICATION_SERIALIZER.toSlime(original)));
-
- assertEquals(original.id(), serialized.id());
- assertEquals(original.createdAt(), serialized.createdAt());
- assertEquals(applicationVersion1, serialized.revisions().last().get());
- assertEquals(applicationVersion1, serialized.revisions().get(serialized.instances().get(id1.instance()).deployments().get(zone1).revision()));
- assertEquals(original.revisions().last(), serialized.revisions().last());
- assertEquals(original.revisions().last().get().compileVersion(), serialized.revisions().last().get().compileVersion());
- assertEquals(original.revisions().last().get().allowedMajor(), serialized.revisions().last().get().allowedMajor());
- assertEquals(original.revisions().last().get().authorEmail(), serialized.revisions().last().get().authorEmail());
- assertEquals(original.revisions().last().get().buildTime(), serialized.revisions().last().get().buildTime());
- assertEquals(original.revisions().last().get().sourceUrl(), serialized.revisions().last().get().sourceUrl());
- assertEquals(original.revisions().last().get().commit(), serialized.revisions().last().get().commit());
- assertEquals(original.revisions().last().get().bundleHash(), serialized.revisions().last().get().bundleHash());
- assertEquals(original.revisions().last().get().obsoleteAt(), serialized.revisions().last().get().obsoleteAt());
- assertEquals(original.revisions().last().get().hasPackage(), serialized.revisions().last().get().hasPackage());
- assertEquals(original.revisions().last().get().shouldSkip(), serialized.revisions().last().get().shouldSkip());
- assertEquals(original.revisions().last().get().description(), serialized.revisions().last().get().description());
- assertEquals(original.revisions().last().get().submittedAt(), serialized.revisions().last().get().submittedAt());
- assertEquals(original.revisions().last().get().risk(), serialized.revisions().last().get().risk());
- assertEquals(original.revisions().withPackage(), serialized.revisions().withPackage());
- assertEquals(original.revisions().production(), serialized.revisions().production());
- assertEquals(original.revisions().development(), serialized.revisions().development());
-
- assertEquals(original.deploymentSpec().xmlForm(), serialized.deploymentSpec().xmlForm());
- assertEquals(original.validationOverrides().xmlForm(), serialized.validationOverrides().xmlForm());
-
- assertEquals(original.projectId(), serialized.projectId());
- assertEquals(original.deploymentIssueId(), serialized.deploymentIssueId());
-
- assertEquals(0, serialized.require(id3.instance()).deployments().size());
- assertEquals(0, serialized.require(id3.instance()).rotations().size());
- assertEquals(RotationStatus.EMPTY, serialized.require(id3.instance()).rotationStatus());
-
- assertEquals(2, serialized.require(id1.instance()).deployments().size());
- assertEquals(original.require(id1.instance()).deployments().get(zone1), serialized.require(id1.instance()).deployments().get(zone1));
- assertEquals(original.require(id1.instance()).deployments().get(zone2), serialized.require(id1.instance()).deployments().get(zone2));
-
- assertEquals(original.require(id1.instance()).jobPause(DeploymentContext.systemTest),
- serialized.require(id1.instance()).jobPause(DeploymentContext.systemTest));
- assertEquals(original.require(id1.instance()).jobPause(DeploymentContext.stagingTest),
- serialized.require(id1.instance()).jobPause(DeploymentContext.stagingTest));
-
- assertEquals(original.ownershipIssueId(), serialized.ownershipIssueId());
- assertEquals(original.userOwner(), serialized.userOwner());
- assertEquals(original.issueOwner(), serialized.issueOwner());
- assertEquals(original.majorVersion(), serialized.majorVersion());
- assertEquals(original.deployKeys(), serialized.deployKeys());
-
- assertEquals(original.require(id1.instance()).rotations(), serialized.require(id1.instance()).rotations());
- assertEquals(original.require(id1.instance()).rotationStatus(), serialized.require(id1.instance()).rotationStatus());
-
- assertEquals(original.require(id1.instance()).change(), serialized.require(id1.instance()).change());
- assertEquals(original.require(id3.instance()).change(), serialized.require(id3.instance()).change());
-
- // Test metrics
- assertEquals(original.metrics().queryServiceQuality(), serialized.metrics().queryServiceQuality(), Double.MIN_VALUE);
- assertEquals(original.metrics().writeServiceQuality(), serialized.metrics().writeServiceQuality(), Double.MIN_VALUE);
- assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().queriesPerSecond(), serialized.require(id1.instance()).deployments().get(zone2).metrics().queriesPerSecond(), Double.MIN_VALUE);
- assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().writesPerSecond(), serialized.require(id1.instance()).deployments().get(zone2).metrics().writesPerSecond(), Double.MIN_VALUE);
- assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().documentCount(), serialized.require(id1.instance()).deployments().get(zone2).metrics().documentCount(), Double.MIN_VALUE);
- assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().queryLatencyMillis(), serialized.require(id1.instance()).deployments().get(zone2).metrics().queryLatencyMillis(), Double.MIN_VALUE);
- assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().writeLatencyMillis(), serialized.require(id1.instance()).deployments().get(zone2).metrics().writeLatencyMillis(), Double.MIN_VALUE);
- assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().instant(), serialized.require(id1.instance()).deployments().get(zone2).metrics().instant());
- assertEquals(original.require(id1.instance()).deployments().get(zone2).metrics().warnings(), serialized.require(id1.instance()).deployments().get(zone2).metrics().warnings());
-
- // Test quota
- assertEquals(original.require(id1.instance()).deployments().get(zone2).quota().rate(), serialized.require(id1.instance()).deployments().get(zone2).quota().rate(), 0.001);
-
- assertEquals(original.require(id1.instance()).deployments().get(zone1).dataPlaneTokens(), serialized.require(id1.instance()).deployments().get(zone1).dataPlaneTokens());
- assertEquals(original.require(id1.instance()).deployments().get(zone2).dataPlaneTokens(), serialized.require(id1.instance()).deployments().get(zone2).dataPlaneTokens());
- }
-
- @Test
- void testCompleteApplicationDeserialization() throws Exception {
- byte[] applicationJson = Files.readAllBytes(testData.resolve("complete-application.json"));
- APPLICATION_SERIALIZER.fromSlime(applicationJson);
- // ok if no error
- }
-
- private static AssignedRotation rotation(String clusterId, String endpointId, String rotationId, Collection<String> regions) {
- return new AssignedRotation(
- new ClusterSpec.Id(clusterId),
- EndpointId.of(endpointId),
- new RotationId(rotationId),
- regions.stream().map(RegionName::from).collect(Collectors.toSet())
- );
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializerTest.java
deleted file mode 100644
index 35fa2d95e1f..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ArchiveBucketsSerializerTest.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveBuckets;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.TenantManagedArchiveBucket;
-import com.yahoo.vespa.hosted.controller.api.integration.archive.VespaManagedArchiveBucket;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class ArchiveBucketsSerializerTest {
-
- @Test
- void serdes() {
- ArchiveBuckets archiveBuckets = new ArchiveBuckets(
- Set.of(new VespaManagedArchiveBucket("bucket1Name", "key1Arn").withTenants(Set.of(TenantName.from("t1"), TenantName.from("t2"))),
- new VespaManagedArchiveBucket("bucket2Name", "key2Arn").withTenant(TenantName.from("t3"))),
- Set.of(new TenantManagedArchiveBucket("bucket3Name", CloudAccount.from("acct-1"), Instant.ofEpochMilli(1234)),
- new TenantManagedArchiveBucket("bucket4Name", CloudAccount.from("acct-2"), Instant.ofEpochMilli(5678))));
-
- assertEquals(archiveBuckets, ArchiveBucketsSerializer.fromSlime(ArchiveBucketsSerializer.toSlime(archiveBuckets)));
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializerTest.java
deleted file mode 100644
index ac33f1d9e3a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/AuditLogSerializerTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLog;
-import org.junit.jupiter.api.Test;
-
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-
-import static java.time.temporal.ChronoUnit.MILLIS;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-public class AuditLogSerializerTest {
-
- @Test
- void test_serialization() {
- Instant i1 = Instant.now();
- Instant i2 = i1.minus(Duration.ofHours(1));
- Instant i3 = i1.minus(Duration.ofHours(2));
- Instant i4 = i1.minus(Duration.ofHours(3));
-
- AuditLog log = new AuditLog(List.of(
- new AuditLog.Entry(i1, AuditLog.Entry.Client.other, "bar", AuditLog.Entry.Method.POST,
- "/bar/baz/",
- "0".repeat(2048).getBytes(StandardCharsets.UTF_8)),
- new AuditLog.Entry(i2, AuditLog.Entry.Client.other, "foo", AuditLog.Entry.Method.POST,
- "/foo/bar/",
- "{\"foo\":\"bar\"}".getBytes(StandardCharsets.UTF_8)),
- new AuditLog.Entry(i3, AuditLog.Entry.Client.hv, "baz", AuditLog.Entry.Method.POST,
- "/foo/baz/",
- new byte[0]),
- new AuditLog.Entry(i4, AuditLog.Entry.Client.console, "baz", AuditLog.Entry.Method.POST,
- "/foo/baz/",
- "000\ufdff\ufeff\uffff000".getBytes(StandardCharsets.UTF_8)), // non-ascii
- new AuditLog.Entry(i4, AuditLog.Entry.Client.cli, "quux", AuditLog.Entry.Method.POST,
- "/foo/quux/",
- new byte[]{(byte) 0xDE, (byte) 0xAD, (byte) 0xBE, (byte) 0xEF}) // garbage
- ));
-
- AuditLogSerializer serializer = new AuditLogSerializer();
- AuditLog serialized = serializer.fromSlime(serializer.toSlime(log));
- assertEquals(log.entries().size(), serialized.entries().size());
-
- for (int i = 0; i < log.entries().size(); i++) {
- AuditLog.Entry entry = log.entries().get(i);
- AuditLog.Entry serializedEntry = serialized.entries().get(i);
-
- assertEquals(entry.at().truncatedTo(MILLIS), serializedEntry.at());
- assertEquals(entry.client(), serializedEntry.client());
- assertEquals(entry.principal(), serializedEntry.principal());
- assertEquals(entry.method(), serializedEntry.method());
- assertEquals(entry.resource(), serializedEntry.resource());
- assertEquals(entry.data(), serializedEntry.data());
- }
-
- assertEquals(1024, log.entries().get(0).data().get().length());
- assertTrue(log.entries().get(2).data().isEmpty());
- assertTrue(log.entries().get(3).data().isEmpty());
- assertTrue(log.entries().get(4).data().isEmpty());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStoreTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStoreTest.java
deleted file mode 100644
index 563f5ba73f8..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/BufferedLogStoreTest.java
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.api.integration.RunDataStore;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockRunDataStore;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.RunLog;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-import java.util.stream.IntStream;
-
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class BufferedLogStoreTest {
-
- @Test
- void chunkingAndFlush() {
- int chunkSize = 1 << 10;
- int maxChunks = 1 << 5;
- CuratorDb buffer = new MockCuratorDb(SystemName.main);
- RunDataStore store = new MockRunDataStore();
- BufferedLogStore logs = new BufferedLogStore(chunkSize, chunkSize * maxChunks, buffer, store);
- RunId id = new RunId(ApplicationId.from("tenant", "application", "instance"),
- DeploymentContext.productionUsWest1,
- 123);
-
- byte[] manyBytes = new byte[chunkSize / 2 + 1]; // One fits, and two (over-)fills.
- Arrays.fill(manyBytes, (byte) 'O');
- LogEntry entry = new LogEntry(0, Instant.ofEpochMilli(123), LogEntry.Type.warning, new String(manyBytes));
-
- // Log entries are re-sequenced by the log store, by enumeration.
- LogEntry entry0 = new LogEntry(0, entry.at(), entry.type(), entry.message());
- LogEntry entry1 = new LogEntry(1, entry.at(), entry.type(), entry.message());
- LogEntry entry2 = new LogEntry(2, entry.at(), entry.type(), entry.message());
- LogEntry entry3 = new LogEntry(3, entry.at(), entry.type(), entry.message());
- LogEntry entry4 = new LogEntry(4, entry.at(), entry.type(), entry.message());
-
- assertEquals(Optional.empty(), logs.readFinished(id, -1));
- assertEquals(RunLog.empty(), logs.readActive(id.application(), id.type(), -1));
-
- logs.append(id.application(), id.type(), Step.deployReal, List.of(entry), false);
- assertEquals(List.of(entry0),
- logs.readActive(id.application(), id.type(), -1).get(Step.deployReal));
- assertEquals(RunLog.empty(), logs.readActive(id.application(), id.type(), 0));
-
- logs.append(id.application(), id.type(), Step.deployReal, List.of(entry), false);
- assertEquals(List.of(entry0, entry1),
- logs.readActive(id.application(), id.type(), -1).get(Step.deployReal));
- assertEquals(List.of(entry1),
- logs.readActive(id.application(), id.type(), 0).get(Step.deployReal));
- assertEquals(RunLog.empty(), logs.readActive(id.application(), id.type(), 1));
-
- logs.append(id.application(), id.type(), Step.deployReal, List.of(entry, entry, entry), false);
- assertEquals(List.of(entry0, entry1, entry2, entry3, entry4),
- logs.readActive(id.application(), id.type(), -1).get(Step.deployReal));
- assertEquals(List.of(entry1, entry2, entry3, entry4),
- logs.readActive(id.application(), id.type(), 0).get(Step.deployReal));
- assertEquals(List.of(entry2, entry3, entry4),
- logs.readActive(id.application(), id.type(), 1).get(Step.deployReal));
- assertEquals(List.of(entry3, entry4),
- logs.readActive(id.application(), id.type(), 2).get(Step.deployReal));
- assertEquals(List.of(entry4),
- logs.readActive(id.application(), id.type(), 3).get(Step.deployReal));
- assertEquals(RunLog.empty(), logs.readActive(id.application(), id.type(), 4));
-
- // We should now have three chunks, with two, two, and one entries.
- assertEquals(Optional.of(4L), buffer.readLastLogEntryId(id.application(), id.type()));
- assertArrayEquals(new long[]{0, 2, 4}, buffer.getLogChunkIds(id.application(), id.type()).toArray());
-
- // Flushing clears the buffer entirely, and stores its aggregated content in the data store.
- logs.flush(id);
- assertEquals(Optional.empty(), buffer.readLastLogEntryId(id.application(), id.type()));
- assertArrayEquals(new long[]{}, buffer.getLogChunkIds(id.application(), id.type()).toArray());
- assertEquals(RunLog.empty(), logs.readActive(id.application(), id.type(), -1));
-
- assertEquals(List.of(entry0, entry1, entry2, entry3, entry4),
- logs.readFinished(id, -1).get().get(Step.deployReal));
- assertEquals(List.of(entry1, entry2, entry3, entry4),
- logs.readFinished(id, 0).get().get(Step.deployReal));
- assertEquals(List.of(entry2, entry3, entry4),
- logs.readFinished(id, 1).get().get(Step.deployReal));
- assertEquals(List.of(entry3, entry4),
- logs.readFinished(id, 2).get().get(Step.deployReal));
- assertEquals(List.of(entry4),
- logs.readFinished(id, 3).get().get(Step.deployReal));
- assertEquals(List.of(), logs.readFinished(id, 4).get().get(Step.deployReal));
-
- // Logging a large entry enough times to reach the maximum size causes no further entries to be stored.
- List<LogEntry> monsterLog = IntStream.range(0, 2 * maxChunks + 3)
- .mapToObj(i -> new LogEntry(i, entry.at(), entry.type(), entry.message()))
- .toList();
- List<LogEntry> logged = new ArrayList<>(monsterLog);
- logged.remove(logged.size() - 1);
- logged.remove(logged.size() - 1);
- logged.remove(logged.size() - 1);
- logged.add(new LogEntry(2 * maxChunks, entry.at(), LogEntry.Type.warning, "Max log size of " + ((chunkSize * maxChunks) >> 20) + "Mb exceeded; further user entries are discarded."));
-
- logs.append(id.application(), id.type(), Step.deployReal, monsterLog, false);
- assertEquals(logged.size(),
- logs.readActive(id.application(), id.type(), -1).get(Step.deployReal).size());
- assertEquals(logged,
- logs.readActive(id.application(), id.type(), -1).get(Step.deployReal));
-
- // An additional, forced entry is appended.
- LogEntry forced = new LogEntry(logged.size(), entry.at(), entry.type(), entry.message());
- logs.append(id.application(), id.type(), Step.deployReal, List.of(forced), true);
- logged.add(forced);
- assertEquals(logged.size(),
- logs.readActive(id.application(), id.type(), -1).get(Step.deployReal).size());
- assertEquals(logged,
- logs.readActive(id.application(), id.type(), -1).get(Step.deployReal));
- logged.remove(logged.size() - 1);
-
- // Flushing the buffer clears it again, and makes it ready for reuse.
- logs.flush(id);
- for (int i = 0; i < 2 * maxChunks + 3; i++)
- logs.append(id.application(), id.type(), Step.deployReal, List.of(entry), false);
- assertEquals(logged.size(),
- logs.readActive(id.application(), id.type(), -1).get(Step.deployReal).size());
- assertEquals(logged,
- logs.readActive(id.application(), id.type(), -1).get(Step.deployReal));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/CertifiedOsVersionSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/CertifiedOsVersionSerializerTest.java
deleted file mode 100644
index 7d21905ea3e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/CertifiedOsVersionSerializerTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.vespa.hosted.controller.versions.CertifiedOsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-import org.junit.jupiter.api.Test;
-
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-class CertifiedOsVersionSerializerTest {
-
- @Test
- public void serialization() {
- Set<CertifiedOsVersion> certifiedVersion = Set.of(new CertifiedOsVersion(new OsVersion(Version.fromString("1.2.3"),
- CloudName.from("cloud1")),
- Version.fromString("4.5.6")),
- new CertifiedOsVersion(new OsVersion(Version.fromString("3.2.1"),
- CloudName.from("cloud2")),
- Version.fromString("6.5.4")));
- CertifiedOsVersionSerializer serializer = new CertifiedOsVersionSerializer();
- assertEquals(certifiedVersion, serializer.fromSlime(serializer.toSlime(certifiedVersion)));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializerTest.java
deleted file mode 100644
index 8c79395b423..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ChangeRequestSerializerTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.time.ZonedDateTime;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author olaa
- */
-public class ChangeRequestSerializerTest {
-
- @Test
- void reserialization_equality() {
- var source = new ChangeRequestSource("aws", "id321", "url", ChangeRequestSource.Status.STARTED, ZonedDateTime.now(), ZonedDateTime.now(), "Some category");
- var actionPlan = List.of(
- new HostAction("host1", HostAction.State.RETIRING, Instant.now()),
- new HostAction("host2", HostAction.State.RETIRED, Instant.now())
- );
-
- var changeRequest = new VespaChangeRequest(
- "id123",
- source,
- List.of("switch1"),
- List.of("host1", "host2"),
- ChangeRequest.Approval.APPROVED,
- ChangeRequest.Impact.VERY_HIGH,
- VespaChangeRequest.Status.IN_PROGRESS,
- actionPlan,
- ZoneId.defaultId()
- );
-
- var reserialized = ChangeRequestSerializer.fromSlime(ChangeRequestSerializer.toSlime(changeRequest));
- assertEquals(changeRequest, reserialized);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializerTest.java
deleted file mode 100644
index 219a4be6143..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ControllerVersionSerializerTest.java
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class ControllerVersionSerializerTest {
-
- private final ControllerVersionSerializer serializer = new ControllerVersionSerializer();
-
- @Test
- void serialization() {
- var version = new ControllerVersion(Version.fromString("7.42.1"), "badc0ffee", Instant.ofEpochSecond(1565876112));
- var serialized = serializer.fromSlime(serializer.toSlime(version));
- assertEquals(version.version(), serialized.version());
- assertEquals(version.commitSha(), serialized.commitSha());
- assertEquals(version.commitDate(), serialized.commitDate());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/DnsChallengeSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/DnsChallengeSerializerTest.java
deleted file mode 100644
index ca22978bae0..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/DnsChallengeSerializerTest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ClusterId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService.DnsChallenge;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService.ChallengeState;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author jonmv
- */
-class DnsChallengeSerializerTest {
-
- private final DnsChallengeSerializer serializer = new DnsChallengeSerializer();
- private final ClusterId clusterId = new ClusterId(new DeploymentId(ApplicationId.defaultId(),
- ZoneId.defaultId()),
- ClusterSpec.Id.from("default"));
- private final DnsChallenge challenge = new DnsChallenge(RecordName.from("name.tld"),
- RecordData.from("1234"),
- clusterId,
- "deadbeef",
- Optional.of(CloudAccount.from("123321123321")),
- Instant.ofEpochMilli(123),
- ChallengeState.pending);
-
- @Test
- void testSerialization() {
- DnsChallenge deserialized = serializer.fromJson(serializer.toJson(challenge), clusterId);
- assertEquals(challenge, deserialized);
- for (ChallengeState state : ChallengeState.values())
- assertEquals(challenge.withState(state), serializer.fromJson(serializer.toJson(challenge.withState(state)), clusterId));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateSerializerTest.java
deleted file mode 100644
index bccc2772da0..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/EndpointCertificateSerializerTest.java
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author andreer
- */
-public class EndpointCertificateSerializerTest {
-
- private final EndpointCertificate sampleWithOptionalFieldsSet =
- new EndpointCertificate("keyName", "certName", 1, 0, "rootRequestId", Optional.of("leafRequestId"), List.of("SAN1", "SAN2"), "issuer", java.util.Optional.of(1628000000L), Optional.of(1612000000L), Optional.empty());
-
- private final EndpointCertificate sampleWithoutOptionalFieldsSet =
- new EndpointCertificate("keyName", "certName", 1, 0, "rootRequestId", Optional.empty(), List.of("SAN1", "SAN2"), "issuer", Optional.empty(), Optional.empty(), Optional.empty());
-
- @Test
- void serialize_with_optional_fields() {
- assertEquals(
- "{\"keyName\":\"keyName\",\"certName\":\"certName\",\"version\":1,\"lastRequested\":0,\"requestId\":\"rootRequestId\",\"leafRequestId\":\"leafRequestId\",\"requestedDnsSans\":[\"SAN1\",\"SAN2\"],\"issuer\":\"issuer\",\"expiry\":1628000000,\"lastRefreshed\":1612000000}",
- EndpointCertificateSerializer.toSlime(sampleWithOptionalFieldsSet).toString());
- }
-
- @Test
- void serialize_without_optional_fields() {
- assertEquals(
- "{\"keyName\":\"keyName\",\"certName\":\"certName\",\"version\":1,\"lastRequested\":0,\"requestId\":\"rootRequestId\",\"requestedDnsSans\":[\"SAN1\",\"SAN2\"],\"issuer\":\"issuer\"}",
- EndpointCertificateSerializer.toSlime(sampleWithoutOptionalFieldsSet).toString());
- }
-
- @Test
- void deserialize_from_json_with_optional_fields() {
- assertEquals(
- sampleWithOptionalFieldsSet,
- EndpointCertificateSerializer.fromJsonString(
- "{\"keyName\":\"keyName\",\"certName\":\"certName\",\"version\":1,\"lastRequested\":0,\"requestId\":\"rootRequestId\",\"leafRequestId\":\"leafRequestId\",\"requestedDnsSans\":[\"SAN1\",\"SAN2\"],\"issuer\":\"issuer\",\"expiry\":1628000000,\"lastRefreshed\":1612000000}"));
- }
-
- @Test
- void deserialize_from_json_without_optional_fields() {
- assertEquals(
- sampleWithoutOptionalFieldsSet,
- EndpointCertificateSerializer.fromJsonString(
- "{\"keyName\":\"keyName\",\"certName\":\"certName\",\"version\":1,\"lastRequested\":0,\"requestId\":\"rootRequestId\",\"requestedDnsSans\":[\"SAN1\",\"SAN2\"],\"issuer\":\"issuer\"}"));
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializerTest.java
deleted file mode 100644
index bab99314a41..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/LogSerializerTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.vespa.hosted.controller.api.integration.LogEntry;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployTester;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author jonmv
- */
-public class LogSerializerTest {
-
- private static final LogSerializer serializer = new LogSerializer();
- private static final Path logsFile = Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/logs.json");
-
- @Test
- void testSerialization() throws IOException {
- for (LogEntry.Type type : LogEntry.Type.values())
- assertEquals(type, LogSerializer.typeOf(LogSerializer.valueOf(type)));
-
- byte[] logJson = Files.readAllBytes(logsFile);
-
- LogEntry first = new LogEntry(0, Instant.ofEpochMilli(0), LogEntry.Type.info, "First");
- LogEntry second = new LogEntry(1, Instant.ofEpochMilli(0), LogEntry.Type.info, "Second");
- LogEntry third = new LogEntry(2, Instant.ofEpochMilli(1000), LogEntry.Type.debug, "Third");
- LogEntry fourth = new LogEntry(3, Instant.ofEpochMilli(2000), LogEntry.Type.warning, "Fourth");
-
- Map<Step, List<LogEntry>> expected = new HashMap<>();
- expected.put(deployReal, new ArrayList<>());
- expected.get(deployReal).add(third);
- expected.put(deployTester, new ArrayList<>());
- expected.get(deployTester).add(fourth);
-
- assertEquals(expected, serializer.fromJson(logJson, 1));
-
- expected.get(deployReal).add(0, first);
- expected.get(deployTester).add(0, second);
- assertEquals(expected, serializer.fromJson(logJson, -1));
-
- assertEquals(expected, serializer.fromJson(serializer.toJson(expected), -1));
-
- expected.get(deployReal).add(first);
- expected.get(deployReal).add(third);
- expected.get(deployTester).add(second);
- expected.get(deployTester).add(fourth);
-
- assertEquals(expected, serializer.fromJson(List.of(logJson, logJson), -1));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/MailVerificationSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/MailVerificationSerializerTest.java
deleted file mode 100644
index 17f0fd950fd..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/MailVerificationSerializerTest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * @author olaa
- */
-public class MailVerificationSerializerTest {
-
- @Test
- public void test_serialization() {
- var original = new PendingMailVerification(TenantName.from("test-tenant"),
- "email@mycomp.any",
- "xyz-123",
- Instant.now().truncatedTo(ChronoUnit.MILLIS),
- PendingMailVerification.MailType.TENANT_CONTACT
- );
-
- var serialized = MailVerificationSerializer.toSlime(original);
- var deserialized = MailVerificationSerializer.fromSlime(serialized);
- assertEquals(original, deserialized);
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java
deleted file mode 100644
index 4aefc5bc368..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NameServiceQueueSerializerTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.LatencyAliasTarget;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.WeightedDirectTarget;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.dns.CreateRecord;
-import com.yahoo.vespa.hosted.controller.dns.CreateRecords;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceRequest;
-import com.yahoo.vespa.hosted.controller.dns.RemoveRecords;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class NameServiceQueueSerializerTest {
-
- private final NameServiceQueueSerializer serializer = new NameServiceQueueSerializer();
-
- @Test
- void test_serialization() {
- Optional<TenantAndApplicationId> owner = Optional.of(TenantAndApplicationId.from("t", "a"));
- var record1 = new Record(Record.Type.CNAME, RecordName.from("cname.example.com"), RecordData.from("example.com"));
- var record2 = new Record(Record.Type.TXT, RecordName.from("txt.example.com"), RecordData.from("text"));
- var requests = List.<NameServiceRequest>of(
- new CreateRecord(owner, record1),
- new CreateRecords(owner, List.of(record2)),
- new CreateRecords(owner, List.of(new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"),
- new LatencyAliasTarget(HostName.of("alias1"),
- "dns-zone-01",
- ZoneId.from("prod", "us-north-1")).pack()),
- new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"),
- new LatencyAliasTarget(HostName.of("alias2"),
- "dns-zone-02",
- ZoneId.from("prod", "us-north-2")).pack()),
- new Record(Record.Type.ALIAS, RecordName.from("alias.example.com"),
- new LatencyAliasTarget(HostName.of("alias2"),
- "ignored",
- ZoneId.from("prod", "us-south-1")).pack()))
- ),
- new CreateRecords(Optional.empty(), List.of(new Record(Record.Type.DIRECT, RecordName.from("direct.example.com"),
- new WeightedDirectTarget(RecordData.from("10.1.2.3"),
- ZoneId.from("prod", "us-north-1"),
- 100).pack()))),
- new RemoveRecords(Optional.empty(), record1.type(), record1.name()),
- new RemoveRecords(owner, record2.type(), record2.name(), Optional.of(record2.data()))
- );
-
- var queue = new NameServiceQueue(requests);
- var serialized = serializer.fromSlime(serializer.toSlime(queue));
- assertEquals(List.copyOf(queue.requests()), List.copyOf(serialized.requests()));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java
deleted file mode 100644
index 65da43a3ec4..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/NotificationsSerializerTest.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.notification.MailTemplating;
-import com.yahoo.vespa.hosted.controller.notification.Notification;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author freva
- */
-public class NotificationsSerializerTest {
-
- @Test
- void serialization_test() throws IOException {
- NotificationsSerializer serializer = new NotificationsSerializer();
- TenantName tenantName = TenantName.from("tenant1");
- var mail = Notification.MailContent.fromTemplate(MailTemplating.Template.DEFAULT_MAIL_CONTENT).subject("My mail subject")
- .with("string-param", "string-value").with("list-param", List.of("elem1", "elem2")).build();
- List<Notification> notifications = List.of(
- new Notification(Instant.ofEpochSecond(1234),
- Notification.Type.applicationPackage,
- Notification.Level.warning,
- NotificationSource.from(TenantAndApplicationId.from(tenantName.value(), "app1")),
- List.of("Something something deprecated...")),
- new Notification(Instant.ofEpochSecond(2345),
- Notification.Type.deployment,
- Notification.Level.error,
- NotificationSource.from(new RunId(ApplicationId.from(tenantName.value(), "app1", "instance1"), DeploymentContext.systemTest, 12)),
- "Failed to deploy", List.of("Node allocation failure"),
- Optional.of(mail)));
-
- Slime serialized = serializer.toSlime(notifications);
- assertEquals("{\"notifications\":[" +
- "{" +
- "\"at\":1234000," +
- "\"type\":\"applicationPackage\"," +
- "\"level\":\"warning\"," +
- "\"title\":\"\"," +
- "\"messages\":[\"Something something deprecated...\"]," +
- "\"application\":\"app1\"" +
- "},{" +
- "\"at\":2345000," +
- "\"type\":\"deployment\"," +
- "\"level\":\"error\"," +
- "\"title\":\"Failed to deploy\"," +
- "\"messages\":[\"Node allocation failure\"]," +
- "\"application\":\"app1\"," +
- "\"instance\":\"instance1\"," +
- "\"jobId\":\"test.us-east-1\"," +
- "\"runNumber\":12," +
- "\"mail-template\":\"default-mail-content\"," +
- "\"mail-subject\":\"My mail subject\"," +
- "\"mail-params\":{\"list-param\":[\"elem1\",\"elem2\"],\"string-param\":\"string-value\"}" +
- "}]}", new String(SlimeUtils.toJsonBytes(serialized)));
-
- List<Notification> deserialized = serializer.fromSlime(tenantName, serialized);
- assertEquals(notifications, deserialized);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionSerializerTest.java
deleted file mode 100644
index 4170dfccfc3..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionSerializerTest.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.google.common.collect.ImmutableSet;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-import org.junit.jupiter.api.Test;
-
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class OsVersionSerializerTest {
-
- @Test
- void test_serialization() {
- OsVersionSerializer serializer = new OsVersionSerializer();
- Set<OsVersion> osVersions = ImmutableSet.of(
- new OsVersion(Version.fromString("7.1"), CloudName.DEFAULT),
- new OsVersion(Version.fromString("7.1"), CloudName.from("foo"))
- );
- Set<OsVersion> serialized = serializer.fromSlime(serializer.toSlime(osVersions));
- assertEquals(osVersions, serialized);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializerTest.java
deleted file mode 100644
index 259e27515f3..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionStatusSerializerTest.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class OsVersionStatusSerializerTest {
-
- @Test
- void serialization() {
- Version version1 = Version.fromString("7.1");
- Version version2 = Version.fromString("7.2");
- Map<OsVersion, List<NodeVersion>> versions = new LinkedHashMap<>();
-
- versions.put(new OsVersion(version1, CloudName.DEFAULT), List.of(
- new NodeVersion(HostName.of("node1"), ZoneId.from("prod", "us-west"), version1, version2, Optional.of(Instant.ofEpochMilli(11))),
- new NodeVersion(HostName.of("node2"), ZoneId.from("prod", "us-east"), version1, version2, Optional.of(Instant.ofEpochMilli(22)))
- ));
- versions.put(new OsVersion(version2, CloudName.DEFAULT), List.of(
- new NodeVersion(HostName.of("node3"), ZoneId.from("prod", "us-west"), version2, version2, Optional.of(Instant.ofEpochMilli(33))),
- new NodeVersion(HostName.of("node4"), ZoneId.from("prod", "us-east"), version2, version2, Optional.of(Instant.ofEpochMilli(44)))
- ));
-
- OsVersionStatusSerializer serializer = new OsVersionStatusSerializer(new OsVersionSerializer(), new NodeVersionSerializer());
- OsVersionStatus status = new OsVersionStatus(versions);
- OsVersionStatus serialized = serializer.fromSlime(serializer.toSlime(status));
- assertEquals(status.versions(), serialized.versions());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionTargetSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionTargetSerializerTest.java
deleted file mode 100644
index 7335a3e525b..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/OsVersionTargetSerializerTest.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.vespa.hosted.controller.versions.OsVersion;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionTarget;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.util.Set;
-import java.util.SortedSet;
-import java.util.TreeSet;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class OsVersionTargetSerializerTest {
-
- @Test
- void serialization() {
- OsVersionTargetSerializer serializer = new OsVersionTargetSerializer(new OsVersionSerializer());
- SortedSet<OsVersionTarget> targets = new TreeSet<>();
- targets.add(new OsVersionTarget(new OsVersion(Version.fromString("7.1"), CloudName.DEFAULT), Instant.ofEpochMilli(123), false, false));
- targets.add(new OsVersionTarget(new OsVersion(Version.fromString("7.1"), CloudName.from("foo")), Instant.ofEpochMilli(456), true, true));
-
- Set<OsVersionTarget> serialized = serializer.fromSlime(serializer.toSlime(targets));
- assertEquals(targets, serialized);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializerTest.java
deleted file mode 100644
index 2ff9e484e57..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RoutingPolicySerializerTest.java
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.GeneratedEndpoint;
-import com.yahoo.vespa.hosted.controller.routing.GeneratedEndpointList;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicy;
-import com.yahoo.vespa.hosted.controller.routing.RoutingPolicyId;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mortent
- */
-public class RoutingPolicySerializerTest {
-
- private final RoutingPolicySerializer serializer = new RoutingPolicySerializer();
-
- @Test
- void serialization() {
- var owner = ApplicationId.defaultId();
- var instanceEndpoints = Set.of(EndpointId.of("r1"), EndpointId.of("r2"));
- var applicationEndpoints = Set.of(EndpointId.of("a1"));
- var id1 = new RoutingPolicyId(owner,
- ClusterSpec.Id.from("my-cluster1"),
- ZoneId.from("prod", "us-north-1"));
- var id2 = new RoutingPolicyId(owner,
- ClusterSpec.Id.from("my-cluster2"),
- ZoneId.from("prod", "us-north-2"));
- var policies = List.of(new RoutingPolicy(id1,
- Optional.of(HostName.of("long-and-ugly-name")),
- Optional.empty(),
- Optional.of("zone1"),
- Set.of(),
- Set.of(),
- RoutingStatus.DEFAULT,
- false,
- GeneratedEndpointList.of(new GeneratedEndpoint("deadbeef", "cafed00d", AuthMethod.mtls, Optional.of(EndpointId.of("foo"))))),
- new RoutingPolicy(id2,
- Optional.of(HostName.of("long-and-ugly-name-2")),
- Optional.empty(),
- Optional.empty(),
- instanceEndpoints,
- Set.of(),
- new RoutingStatus(RoutingStatus.Value.out,
- RoutingStatus.Agent.tenant,
- Instant.ofEpochSecond(123)),
- true,
- GeneratedEndpointList.of(new GeneratedEndpoint("cafed00d", "deadbeef", AuthMethod.token, Optional.empty()))),
- new RoutingPolicy(id1,
- Optional.empty(),
- Optional.of("127.0.0.1"),
- Optional.of("zone2"),
- instanceEndpoints,
- applicationEndpoints,
- RoutingStatus.DEFAULT,
- true,
- GeneratedEndpointList.EMPTY));
- var serialized = serializer.fromSlime(owner, serializer.toSlime(policies));
- assertEquals(policies.size(), serialized.size());
- for (Iterator<RoutingPolicy> it1 = policies.iterator(), it2 = serialized.iterator(); it1.hasNext(); ) {
- var expected = it1.next();
- var actual = it2.next();
- assertEquals(expected.id(), actual.id());
- assertEquals(expected.canonicalName(), actual.canonicalName());
- assertEquals(expected.ipAddress(), actual.ipAddress());
- assertEquals(expected.dnsZone(), actual.dnsZone());
- assertEquals(expected.instanceEndpoints(), actual.instanceEndpoints());
- assertEquals(expected.applicationEndpoints(), actual.applicationEndpoints());
- assertEquals(expected.routingStatus(), actual.routingStatus());
- assertEquals(expected.isPublic(), actual.isPublic());
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java
deleted file mode 100644
index 7a9a1795444..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/RunSerializerTest.java
+++ /dev/null
@@ -1,160 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.google.common.collect.ImmutableMap;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.deployment.ConvergenceSummary;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.JobProfile;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.deployment.Run.Reason;
-import com.yahoo.vespa.hosted.controller.deployment.RunStatus;
-import com.yahoo.vespa.hosted.controller.deployment.Step;
-import com.yahoo.vespa.hosted.controller.deployment.StepInfo;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.aborted;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.failed;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.succeeded;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.Status.unfinished;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.copyVespaLogs;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deactivateTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployInitialReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.deployTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.endStagingSetup;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.endTests;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installInitialReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installReal;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.installTester;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.report;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.startStagingSetup;
-import static com.yahoo.vespa.hosted.controller.deployment.Step.startTests;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.time.temporal.ChronoUnit.MILLIS;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class RunSerializerTest {
-
- private static final RunSerializer serializer = new RunSerializer();
- private static final Path runFile = Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/run-status.json");
- private static final RunId id = new RunId(ApplicationId.from("tenant", "application", "default"),
- DeploymentContext.productionUsEast3,
- 112358);
- private static final Instant start = Instant.parse("2007-12-03T10:15:30.00Z");
-
- @Test
- void testSerialization() throws IOException {
- for (Step step : Step.values())
- assertEquals(step, RunSerializer.stepOf(RunSerializer.valueOf(step)));
-
- for (Step.Status status : Step.Status.values())
- assertEquals(status, RunSerializer.stepStatusOf(RunSerializer.valueOf(status)));
-
- for (RunStatus status : RunStatus.values())
- assertEquals(status, RunSerializer.runStatusOf(RunSerializer.valueOf(status)));
-
- // The purpose of this serialised data is to ensure a new format does not break everything, so keep it up to date!
- Run run = serializer.runsFromSlime(SlimeUtils.jsonToSlime(Files.readAllBytes(runFile))).get(id);
- for (Step step : Step.values())
- assertTrue(run.steps().containsKey(step));
-
- assertEquals(id, run.id());
- assertEquals(start, run.start());
- assertEquals(Optional.of(Instant.ofEpochMilli(321321321321L)), run.noNodesDownSince());
- assertFalse(run.hasEnded());
- assertEquals(running, run.status());
- assertEquals(3, run.lastTestLogEntry());
- Version version1 = new Version(1, 2, 3);
- assertEquals(version1, run.versions().targetPlatform());
- RevisionId revision1 = RevisionId.forDevelopment(123, id.job());
- RevisionId revision2 = RevisionId.forProduction(122);
- assertEquals(revision1, run.versions().targetRevision());
- assertEquals(new Reason(Optional.of("because"),
- Optional.of(new JobId(id.application(), id.type())),
- Optional.of(Change.of(version1).with(revision2))),
- run.reason());
- assertEquals(new Version(1, 2, 2), run.versions().sourcePlatform().get());
- assertEquals(revision2, run.versions().sourceRevision().get());
- assertEquals(Optional.of(new ConvergenceSummary(1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233)),
- run.convergenceSummary());
- assertEquals(X509CertificateUtils.fromPem("-----BEGIN CERTIFICATE-----\n" +
- "MIIBEzCBu6ADAgECAgEBMAoGCCqGSM49BAMEMBQxEjAQBgNVBAMTCW15c2Vydmlj\n" +
- "ZTAeFw0xOTA5MDYwNzM3MDZaFw0xOTA5MDcwNzM3MDZaMBQxEjAQBgNVBAMTCW15\n" +
- "c2VydmljZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABM0JhD8fV2DlAkjQOGX3\n" +
- "Y50ryMBr3g2+v/uFiRoxJ1muuSOWYrW7HCQIGuzc04fa0QwtaX/voAZKCV51t6jF\n" +
- "0fwwCgYIKoZIzj0EAwQDRwAwRAIgVbQ3Co1H4X0gmRrtXSyTU0HgBQu9PXHMmX20\n" +
- "5MyyPSoCIBltOcmaPfdN03L3zqbqZ6PgUBWsvAHgiBzL3hrtJ+iy\n" +
- "-----END CERTIFICATE-----"),
- run.testerCertificate().get());
- assertEquals(Optional.empty(), run.cloudAccount());
- assertEquals(ImmutableMap.<Step, StepInfo>builder()
- .put(deployInitialReal, new StepInfo(deployInitialReal, unfinished, Optional.empty()))
- .put(installInitialReal, new StepInfo(installInitialReal, failed, Optional.of(Instant.ofEpochMilli(1196676940000L))))
- .put(deployReal, new StepInfo(deployReal, succeeded, Optional.empty()))
- .put(installReal, new StepInfo(installReal, unfinished, Optional.empty()))
- .put(deactivateReal, new StepInfo(deactivateReal, failed, Optional.empty()))
- .put(deployTester, new StepInfo(deployTester, succeeded, Optional.empty()))
- .put(installTester, new StepInfo(installTester, unfinished, Optional.of(Instant.ofEpochMilli(1196677940000L))))
- .put(deactivateTester, new StepInfo(deactivateTester, failed, Optional.empty()))
- .put(copyVespaLogs, new StepInfo(copyVespaLogs, succeeded, Optional.empty()))
- .put(startStagingSetup, new StepInfo(startStagingSetup, succeeded, Optional.empty()))
- .put(endStagingSetup, new StepInfo(endStagingSetup, unfinished, Optional.empty()))
- .put(startTests, new StepInfo(startTests, succeeded, Optional.empty()))
- .put(endTests, new StepInfo(endTests, unfinished, Optional.empty()))
- .put(report, new StepInfo(report, failed, Optional.empty()))
- .build(),
- run.steps());
-
- run = run.with(1L << 50)
- .with(Instant.now().truncatedTo(MILLIS))
- .noNodesDownSince(Instant.now().truncatedTo(MILLIS))
- .aborted(false)
- .with(CloudAccount.from("gcp:foobar"))
- .finished(Instant.now().truncatedTo(MILLIS));
- assertEquals(aborted, run.status());
- assertTrue(run.hasEnded());
-
- Run phoenix = serializer.runsFromSlime(serializer.toSlime(List.of(run))).get(id);
- assertEquals(run.id(), phoenix.id());
- assertEquals(run.start(), phoenix.start());
- assertEquals(run.end(), phoenix.end());
- assertEquals(run.status(), phoenix.status());
- assertEquals(run.lastTestLogEntry(), phoenix.lastTestLogEntry());
- assertEquals(run.lastVespaLogTimestamp(), phoenix.lastVespaLogTimestamp());
- assertEquals(run.noNodesDownSince(), phoenix.noNodesDownSince());
- assertEquals(run.testerCertificate(), phoenix.testerCertificate());
- assertEquals(run.versions(), phoenix.versions());
- assertEquals(run.steps(), phoenix.steps());
- assertEquals(run.isDryRun(), phoenix.isDryRun());
- assertEquals(run.reason(), phoenix.reason());
-
- assertEquals(new String(SlimeUtils.toJsonBytes(serializer.toSlime(run).get(), false), UTF_8),
- new String(SlimeUtils.toJsonBytes(serializer.toSlime(phoenix).get(), false), UTF_8));
-
- Run initial = Run.initial(id, run.versions(), run.isRedeployment(), run.start(), JobProfile.production,
- new Reason(Optional.empty(), Optional.empty(), Optional.empty()));
- assertEquals(initial, serializer.runFromSlime(serializer.toSlime(initial)));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializerTest.java
deleted file mode 100644
index 5caf6684a48..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/SupportAccessSerializerTest.java
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.security.KeyAlgorithm;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.security.SignatureAlgorithm;
-import com.yahoo.security.X509CertificateBuilder;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.slime.JsonFormat;
-import com.yahoo.slime.Slime;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccess;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccessGrant;
-import org.junit.jupiter.api.Test;
-
-import javax.security.auth.x500.X500Principal;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.math.BigInteger;
-import java.security.cert.X509Certificate;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class SupportAccessSerializerTest {
-
- private final X509Certificate cert_3_to_4 = grantCertificate(hour(3), hour(4));
- private final X509Certificate cert_7_to_19 = grantCertificate(hour(7), hour(19));
-
- SupportAccess supportAccessExample = SupportAccess.DISALLOWED_NO_HISTORY
- .withAllowedUntil(hour(24), "andreer", hour(2))
- .withGrant(new SupportAccessGrant("mortent", cert_3_to_4))
- .withGrant(new SupportAccessGrant("mortent", cert_7_to_19))
- .withDisallowed("andreer", hour(22))
- .withAllowedUntil(hour(36), "andreer", hour(30));
-
- private final String expectedWithCertificates = "{\n"
- + " \"history\": [\n"
- + " {\n"
- + " \"state\": \"allowed\",\n"
- + " \"at\": \"1970-01-02T06:00:00Z\",\n"
- + " \"until\": \"1970-01-02T12:00:00Z\",\n"
- + " \"by\": \"andreer\"\n"
- + " },\n"
- + " {\n"
- + " \"state\": \"disallowed\",\n"
- + " \"at\": \"1970-01-01T22:00:00Z\",\n"
- + " \"by\": \"andreer\"\n"
- + " },\n"
- + " {\n"
- + " \"state\": \"allowed\",\n"
- + " \"at\": \"1970-01-01T02:00:00Z\",\n"
- + " \"until\": \"1970-01-02T00:00:00Z\",\n"
- + " \"by\": \"andreer\"\n"
- + " }\n"
- + " ],\n"
- + " \"grants\": [\n"
- + " {\n"
- + " \"requestor\": \"mortent\",\n"
- + " \"certificate\": \"" + toPem(cert_7_to_19) + "\",\n"
- + " \"notBefore\": \"1970-01-01T07:00:00Z\",\n"
- + " \"notAfter\": \"1970-01-01T19:00:00Z\"\n"
- + " },\n"
- + " {\n"
- + " \"requestor\": \"mortent\",\n"
- + " \"certificate\": \"" + toPem(cert_3_to_4) + "\",\n"
- + " \"notBefore\": \"1970-01-01T03:00:00Z\",\n"
- + " \"notAfter\": \"1970-01-01T04:00:00Z\"\n"
- + " }\n"
- + " ]\n"
- + "}\n";
-
- public String toPem(X509Certificate cert) {
- return X509CertificateUtils.toPem(cert).replace("\n", "\\n");
- }
-
- @Test
- void serialize_default() {
- var slime = SupportAccessSerializer.serializeCurrentState(SupportAccess.DISALLOWED_NO_HISTORY, Instant.EPOCH);
- assertSerialized(slime, "{\n" +
- " \"state\": {\n" +
- " \"supportAccess\": \"NOT_ALLOWED\"\n" +
- " },\n" +
- " \"history\": [ ],\n" +
- " \"grants\": [ ]\n" +
- "}\n");
- }
-
- @Test
- void serialize_with_certificates() {
- var slime = SupportAccessSerializer.toSlime(supportAccessExample);
- assertSerialized(slime, expectedWithCertificates);
- }
-
- @Test
- void serialize_with_status() {
- var slime = SupportAccessSerializer.serializeCurrentState(supportAccessExample, hour(12));
- assertSerialized(slime,
- "{\n"
- + " \"state\": {\n"
- + " \"supportAccess\": \"ALLOWED\",\n"
- + " \"until\": \"1970-01-02T12:00:00Z\",\n"
- + " \"by\": \"andreer\"\n"
- + " },\n"
- + " \"history\": [\n"
- + " {\n"
- + " \"state\": \"allowed\",\n"
- + " \"at\": \"1970-01-02T06:00:00Z\",\n"
- + " \"until\": \"1970-01-02T12:00:00Z\",\n"
- + " \"by\": \"andreer\"\n"
- + " },\n"
- + " {\n"
- + " \"state\": \"disallowed\",\n"
- + " \"at\": \"1970-01-01T22:00:00Z\",\n"
- + " \"by\": \"andreer\"\n"
- + " },\n"
- + " {\n"
- + " \"state\": \"allowed\",\n"
- + " \"at\": \"1970-01-01T02:00:00Z\",\n"
- + " \"until\": \"1970-01-02T00:00:00Z\",\n"
- + " \"by\": \"andreer\"\n"
- + " },\n"
- + " {\n"
- + " \"state\": \"grant\",\n"
- + " \"at\": \"1970-01-01T03:00:00Z\",\n"
- + " \"until\": \"1970-01-01T04:00:00Z\",\n"
- + " \"by\": \"mortent\"\n"
- + " }\n"
- + " ],\n"
- + " \"grants\": [\n"
- + " {\n"
- + " \"requestor\": \"mortent\",\n"
- + " \"notBefore\": \"1970-01-01T07:00:00Z\",\n"
- + " \"notAfter\": \"1970-01-01T19:00:00Z\"\n"
- + " }\n"
- + " ]\n"
- + "}\n");
- }
-
- @Test
- void deserialize() {
- var slime = SupportAccessSerializer.toSlime(supportAccessExample);
- assertSerialized(slime, expectedWithCertificates);
-
- var deserialized = SupportAccessSerializer.fromSlime(slime);
- assertEquals(supportAccessExample, deserialized);
- }
-
- private Instant hour(long h) {
- return Instant.EPOCH.plus(h, ChronoUnit.HOURS);
- }
-
- private void assertSerialized(Slime slime, String expected) {
- try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
- new JsonFormat(false).encode(out, slime);
- assertEquals(expected, out.toString());
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- static X509Certificate grantCertificate(Instant notBefore, Instant notAfter) {
- return X509CertificateBuilder
- .fromKeypair(
- KeyUtils.generateKeypair(KeyAlgorithm.EC, 256), new X500Principal("CN=mysubject"),
- notBefore, notAfter, SignatureAlgorithm.SHA256_WITH_ECDSA, BigInteger.valueOf(1))
- .build();
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
deleted file mode 100644
index 493d4df90a9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/TenantSerializerTest.java
+++ /dev/null
@@ -1,359 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.google.common.collect.ImmutableBiMap;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.BillingReference;
-import com.yahoo.vespa.hosted.controller.tenant.CloudAccountInfo;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.DeletedTenant;
-import com.yahoo.vespa.hosted.controller.tenant.Email;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.tenant.PurchaseOrder;
-import com.yahoo.vespa.hosted.controller.tenant.TaxId;
-import com.yahoo.vespa.hosted.controller.tenant.TenantAddress;
-import com.yahoo.vespa.hosted.controller.tenant.TenantBilling;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContact;
-import com.yahoo.vespa.hosted.controller.tenant.TenantContacts;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-import com.yahoo.vespa.hosted.controller.tenant.TermsOfServiceApproval;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.security.PublicKey;
-import java.time.Instant;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-public class TenantSerializerTest {
-
- private static final TenantSerializer serializer = new TenantSerializer();
- private static final PublicKey publicKey = KeyUtils.fromPemEncodedPublicKey("-----BEGIN PUBLIC KEY-----\n" +
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" +
- "z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
- "-----END PUBLIC KEY-----\n");
- private static final PublicKey otherPublicKey = KeyUtils.fromPemEncodedPublicKey("-----BEGIN PUBLIC KEY-----\n" +
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\n" +
- "pDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n" +
- "-----END PUBLIC KEY-----\n");
-
- @Test
- void athenz_tenant() {
- AthenzTenant tenant = AthenzTenant.create(TenantName.from("athenz-tenant"),
- new AthenzDomain("domain1"),
- new Property("property1"),
- Optional.of(new PropertyId("1")),
- Instant.ofEpochMilli(1234L));
- AthenzTenant serialized = (AthenzTenant) serializer.tenantFrom(serializer.toSlime(tenant));
- assertEquals(tenant.name(), serialized.name());
- assertEquals(tenant.domain(), serialized.domain());
- assertEquals(tenant.property(), serialized.property());
- assertTrue(serialized.propertyId().isPresent());
- assertEquals(tenant.propertyId(), serialized.propertyId());
- assertEquals(tenant.createdAt(), serialized.createdAt());
- }
-
- @Test
- void athenz_tenant_without_property_id() {
- AthenzTenant tenant = AthenzTenant.create(TenantName.from("athenz-tenant"),
- new AthenzDomain("domain1"),
- new Property("property1"),
- Optional.empty(),
- Instant.EPOCH);
- AthenzTenant serialized = (AthenzTenant) serializer.tenantFrom(serializer.toSlime(tenant));
- assertFalse(serialized.propertyId().isPresent());
- assertEquals(tenant.propertyId(), serialized.propertyId());
- }
-
- @Test
- void athenz_tenant_with_contact() {
- AthenzTenant tenant = new AthenzTenant(TenantName.from("athenz-tenant"),
- new AthenzDomain("domain1"),
- new Property("property1"),
- Optional.of(new PropertyId("1")),
- Optional.of(contact()),
- Instant.EPOCH,
- lastLoginInfo(321L, 654L, 987L),
- Instant.EPOCH,
- List.of());
- AthenzTenant serialized = (AthenzTenant) serializer.tenantFrom(serializer.toSlime(tenant));
- assertEquals(tenant.contact(), serialized.contact());
- }
-
- @Test
- void cloud_tenant() {
- CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"),
- Instant.ofEpochMilli(1234L),
- lastLoginInfo(123L, 456L, null),
- Optional.of(new SimplePrincipal("foobar-user")),
- ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
- otherPublicKey, new SimplePrincipal("jane")),
- TenantInfo.empty(),
- List.of(),
- new ArchiveAccess(),
- Optional.empty(),
- Instant.EPOCH,
- List.of(),
- Optional.empty(),
- PlanId.from("none"));
- CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
- assertEquals(tenant.name(), serialized.name());
- assertEquals(tenant.creator(), serialized.creator());
- assertEquals(tenant.developerKeys(), serialized.developerKeys());
- assertEquals(tenant.createdAt(), serialized.createdAt());
- assertEquals("none", serialized.planId().value());
- }
-
- @Test
- void cloud_tenant_with_info() {
- CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"),
- Instant.EPOCH,
- lastLoginInfo(null, 789L, 654L),
- Optional.of(new SimplePrincipal("foobar-user")),
- ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
- otherPublicKey, new SimplePrincipal("jane")),
- TenantInfo.empty().withName("Ofni Tnanet"),
- List.of(
- new TenantSecretStore("ss1", "123", "role1"),
- new TenantSecretStore("ss2", "124", "role2")
- ),
- new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role"),
- Optional.of(Instant.ofEpochMilli(1234567)),
- Instant.EPOCH,
- List.of(),
- Optional.empty(),
- PlanId.from("none"));
- CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
- assertEquals(tenant.info(), serialized.info());
- assertEquals(tenant.tenantSecretStores(), serialized.tenantSecretStores());
- assertEquals(tenant.invalidateUserSessionsBefore(), serialized.invalidateUserSessionsBefore());
- }
-
- @Test
- void cloud_tenant_with_old_archive_access_serialization() {
- var json = "{\n" +
- " \"name\": \"elderly-lady\",\n" +
- " \"type\": \"cloud\",\n" +
- " \"createdAt\": 1234,\n" +
- " \"lastLoginInfo\": {\n" +
- " \"user\": 123,\n" +
- " \"developer\": 456\n" +
- " },\n" +
- " \"creator\": \"foobar-user\",\n" +
- " \"pemDeveloperKeys\": [\n" +
- " {\n" +
- " \"key\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\\n-----END PUBLIC KEY-----\\n\",\n" +
- " \"user\": \"joe\"\n" +
- " },\n" +
- " {\n" +
- " \"key\": \"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\\n-----END PUBLIC KEY-----\\n\",\n" +
- " \"user\": \"jane\"\n" +
- " }\n" +
- " ],\n" +
- " \"billingInfo\": {\n" +
- " \"customerId\": \"customer\",\n" +
- " \"productCode\": \"Vespa\"\n" +
- " },\n" +
- " \"archiveAccessRole\": \"arn:aws:iam::123456789012:role/my-role\"\n" +
- "}";
- var tenant = (CloudTenant) serializer.tenantFrom(SlimeUtils.jsonToSlime(json));
- assertEquals("arn:aws:iam::123456789012:role/my-role", tenant.archiveAccess().awsRole().get());
- assertFalse(tenant.archiveAccess().gcpMember().isPresent());
- }
-
- @Test
- void cloud_tenant_with_archive_access() {
- CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"),
- Instant.ofEpochMilli(1234L),
- lastLoginInfo(123L, 456L, null),
- Optional.of(new SimplePrincipal("foobar-user")),
- ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
- otherPublicKey, new SimplePrincipal("jane")),
- TenantInfo.empty(),
- List.of(),
- new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role").withGCPMember("user:foo@example.com"),
- Optional.empty(),
- Instant.EPOCH,
- List.of(new CloudAccountInfo(CloudAccount.from("aws:123456789012"), Version.fromString("1.2.3")),
- new CloudAccountInfo(CloudAccount.from("gcp:my-project"), Version.fromString("3.2.1"))),
- Optional.empty(),
- PlanId.from("none"));
- CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
- assertEquals(serialized.archiveAccess().awsRole().get(), "arn:aws:iam::123456789012:role/my-role");
- assertEquals(serialized.archiveAccess().gcpMember().get(), "user:foo@example.com");
- }
-
- @Test
- void cloud_tenant_with_tenant_info_partial() {
- TenantInfo partialInfo = TenantInfo.empty()
- .withAddress(TenantAddress.empty().withCity("Hønefoss"));
-
- Slime slime = new Slime();
- Cursor parentObject = slime.setObject();
- serializer.toSlime(partialInfo, parentObject);
- assertEquals("{\"info\":{\"name\":\"\",\"email\":\"\",\"website\":\"\",\"contactName\":\"\",\"contactEmail\":\"\",\"contactEmailVerified\":false,\"address\":{\"addressLines\":\"\",\"postalCodeOrZip\":\"\",\"city\":\"Hønefoss\",\"stateRegionProvince\":\"\",\"country\":\"\"}}}", slime.toString());
- }
-
- @Test
- void cloud_tenant_with_tenant_info_full() {
- TenantInfo fullInfo = TenantInfo.empty()
- .withName("My Company")
- .withEmail("email@mycomp.any")
- .withWebsite("http://mycomp.any")
- .withContact(TenantContact.from("My Name", new Email("ceo@mycomp.any", true)))
- .withAddress(TenantAddress.empty()
- .withCity("Hønefoss")
- .withAddress("Riperbakken 2")
- .withCountry("Norway")
- .withCode("3510")
- .withRegion("Viken"))
- .withBilling(TenantBilling.empty()
- .withContact(TenantContact.from("Thomas The Tank Engine", new Email("ceo@mycomp.any", false), "NA"))
- .withAddress(TenantAddress.empty()
- .withCity("Suddery")
- .withCountry("Sodor")
- .withAddress("Central Station")
- .withRegion("Irish Sea"))
- .withPurchaseOrder(new PurchaseOrder("PO42"))
- .withTaxId(new TaxId("NO", "no_vat", "123456789MVA"))
- .withInvoiceEmail(new Email("billing@mycomp.any", false))
- .withToSApproval(new TermsOfServiceApproval(Instant.ofEpochMilli(1234L), new SimplePrincipal("ceo@mycomp.any")))
- );
-
- Slime slime = new Slime();
- Cursor parentCursor = slime.setObject();
- serializer.toSlime(fullInfo, parentCursor);
- TenantInfo roundTripInfo = serializer.tenantInfoFromSlime(parentCursor.field("info"));
-
- assertEquals(fullInfo, roundTripInfo);
- }
-
- @Test
- void cloud_tenant_with_tenant_info_contacts() {
- TenantInfo tenantInfo = TenantInfo.empty()
- .withContacts(new TenantContacts(List.of(
- new TenantContacts.EmailContact(List.of(TenantContacts.Audience.TENANT), new Email("email1@email.com", true)),
- new TenantContacts.EmailContact(List.of(TenantContacts.Audience.TENANT, TenantContacts.Audience.NOTIFICATIONS), new Email("email2@email.com", true)))));
- Slime slime = new Slime();
- Cursor parentCursor = slime.setObject();
- serializer.toSlime(tenantInfo, parentCursor);
- TenantInfo roundTripInfo = serializer.tenantInfoFromSlime(parentCursor.field("info"));
- assertEquals(tenantInfo, roundTripInfo);
- }
-
- @Test
- void cloud_tenant_with_plan_id() {
- CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"),
- Instant.ofEpochMilli(1234L),
- lastLoginInfo(123L, 456L, null),
- Optional.of(new SimplePrincipal("foobar-user")),
- ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
- otherPublicKey, new SimplePrincipal("jane")),
- TenantInfo.empty(),
- List.of(),
- new ArchiveAccess(),
- Optional.empty(),
- Instant.EPOCH,
- List.of(),
- Optional.empty(),
- PlanId.from("pay-as-you-go"));
- CloudTenant serialized = (CloudTenant) serializer.tenantFrom(serializer.toSlime(tenant));
- assertEquals(tenant.name(), serialized.name());
- assertEquals(tenant.creator(), serialized.creator());
- assertEquals(tenant.developerKeys(), serialized.developerKeys());
- assertEquals(tenant.createdAt(), serialized.createdAt());
- assertEquals(tenant.planId(), serialized.planId());
- }
-
- @Test
- void deleted_tenant() {
- DeletedTenant tenant = new DeletedTenant(
- TenantName.from("tenant1"), Instant.ofEpochMilli(1234L), Instant.ofEpochMilli(2345L));
- DeletedTenant serialized = (DeletedTenant) serializer.tenantFrom(serializer.toSlime(tenant));
- assertEquals(tenant.name(), serialized.name());
- assertEquals(tenant.createdAt(), serialized.createdAt());
- assertEquals(tenant.deletedAt(), serialized.deletedAt());
- }
-
- @Test
- void tenant_with_roles_maintained() {
- AthenzTenant tenant = new AthenzTenant(TenantName.from("athenz-tenant"),
- new AthenzDomain("domain1"),
- new Property("property1"),
- Optional.of(new PropertyId("1")),
- Optional.of(contact()),
- Instant.EPOCH,
- lastLoginInfo(321L, 654L, 987L),
- Instant.ofEpochMilli(1_000_000),
- List.of());
- assertEquals(tenant, serializer.tenantFrom(serializer.toSlime(tenant)));
- }
-
- @Test
- void tenant_with_billing_reference() {
- BillingReference reference = new BillingReference("abcdefg", Instant.now());
- CloudTenant tenant = new CloudTenant(TenantName.from("elderly-lady"),
- Instant.ofEpochMilli(1234L),
- lastLoginInfo(123L, 456L, null),
- Optional.of(new SimplePrincipal("foobar-user")),
- ImmutableBiMap.of(publicKey, new SimplePrincipal("joe"),
- otherPublicKey, new SimplePrincipal("jane")),
- TenantInfo.empty(),
- List.of(),
- new ArchiveAccess().withAWSRole("arn:aws:iam::123456789012:role/my-role").withGCPMember("user:foo@example.com"),
- Optional.empty(),
- Instant.EPOCH,
- List.of(),
- Optional.of(reference),
- PlanId.from("none"));
- var slime = serializer.toSlime(tenant);
- var deserialized = serializer.tenantFrom(slime);
- assertEquals(tenant, deserialized);
- }
-
- private static Contact contact() {
- return new Contact(
- URI.create("http://contact1.test"),
- URI.create("http://property1.test"),
- URI.create("http://issue-tracker-1.test"),
- List.of(
- List.of("person1"),
- List.of("person2")
- ),
- "queue",
- Optional.empty()
- );
- }
-
- private static LastLoginInfo lastLoginInfo(Long user, Long developer, Long administrator) {
- Map<LastLoginInfo.UserLevel, Instant> lastLogins = new HashMap<>();
- Optional.ofNullable(user).map(Instant::ofEpochMilli).ifPresent(i -> lastLogins.put(LastLoginInfo.UserLevel.user, i));
- Optional.ofNullable(developer).map(Instant::ofEpochMilli).ifPresent(i -> lastLogins.put(LastLoginInfo.UserLevel.developer, i));
- Optional.ofNullable(administrator).map(Instant::ofEpochMilli).ifPresent(i -> lastLogins.put(LastLoginInfo.UserLevel.administrator, i));
- return new LastLoginInfo(lastLogins);
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/UnassignedCertificateSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/UnassignedCertificateSerializerTest.java
deleted file mode 100644
index c1cdef6e4ee..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/UnassignedCertificateSerializerTest.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.certificate.UnassignedCertificate;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-import java.util.Optional;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-class UnassignedCertificateSerializerTest {
-
- @Test
- public void serialization() {
- EndpointCertificate certificate = new EndpointCertificate("keyName", "certName", 1, 0,
- "rootRequestId", Optional.of("leafRequestId"),
- List.of("SAN1", "SAN2"), "issuer", Optional.of(3L),
- Optional.of(4L), Optional.of("my-id"));
- UnassignedCertificate unassignedCertificate = new UnassignedCertificate(certificate, UnassignedCertificate.State.ready);
- UnassignedCertificateSerializer serializer = new UnassignedCertificateSerializer();
- assertEquals(unassignedCertificate, serializer.fromSlime(serializer.toSlime(unassignedCertificate)));
- }
-
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
deleted file mode 100644
index 450db8fd36e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/VersionStatusSerializerTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
-import static java.time.temporal.ChronoUnit.MILLIS;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class VersionStatusSerializerTest {
-
- @Test
- void testSerialization() {
- List<VespaVersion> vespaVersions = new ArrayList<>();
- Version version = Version.fromString("5.0");
- vespaVersions.add(new VespaVersion(version, "dead", Instant.now(), false, false,
- true, nodeVersions(Version.fromString("5.0"), Version.fromString("5.1"),
- "cfg1", "cfg2", "cfg3"), VespaVersion.Confidence.normal));
- vespaVersions.add(new VespaVersion(version, "cafe", Instant.now(), true, true,
- false, nodeVersions(Version.fromString("5.0"), Version.fromString("5.1"),
- "cfg1", "cfg2", "cfg3"), VespaVersion.Confidence.normal));
- VersionStatus status = new VersionStatus(vespaVersions, 5);
- VersionStatusSerializer serializer = new VersionStatusSerializer(new NodeVersionSerializer());
- VersionStatus deserialized = serializer.fromSlime(serializer.toSlime(status));
-
- assertEquals(status.versions().size(), deserialized.versions().size());
- for (int i = 0; i < status.versions().size(); i++) {
- VespaVersion a = status.versions().get(i);
- VespaVersion b = deserialized.versions().get(i);
- assertEquals(a.releaseCommit(), b.releaseCommit());
- assertEquals(a.committedAt().truncatedTo(MILLIS), b.committedAt());
- assertEquals(a.isControllerVersion(), b.isControllerVersion());
- assertEquals(a.isSystemVersion(), b.isSystemVersion());
- assertEquals(a.isReleased(), b.isReleased());
- assertEquals(a.versionNumber(), b.versionNumber());
- assertEquals(a.nodeVersions(), b.nodeVersions());
- assertEquals(a.confidence(), b.confidence());
- }
- assertEquals(status.currentMajor(), deserialized.currentMajor());
-
- }
-
- private static List<NodeVersion> nodeVersions(Version version, Version wantedVersion, String... hostnames) {
- var nodeVersions = new ArrayList<NodeVersion>();
- for (var hostname : hostnames) {
- nodeVersions.add(new NodeVersion(HostName.of(hostname), ZoneId.from("prod", "us-north-1"), version, wantedVersion, Optional.empty()));
- }
- return nodeVersions;
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ZoneRoutingPolicySerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ZoneRoutingPolicySerializerTest.java
deleted file mode 100644
index 7bbdbe33e6c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/ZoneRoutingPolicySerializerTest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.persistence;
-
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-import com.yahoo.vespa.hosted.controller.routing.ZoneRoutingPolicy;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class ZoneRoutingPolicySerializerTest {
-
- @Test
- void serialization() {
- var serializer = new ZoneRoutingPolicySerializer(new RoutingPolicySerializer());
- var zone = ZoneId.from("prod", "us-north-1");
- var policy = new ZoneRoutingPolicy(zone,
- RoutingStatus.create(RoutingStatus.Value.out, RoutingStatus.Agent.operator,
- Instant.ofEpochMilli(123)));
- var serialized = serializer.fromSlime(zone, serializer.toSlime(policy));
- assertEquals(policy, serialized);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json
deleted file mode 100644
index 32f7e8e8f5a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/complete-application.json
+++ /dev/null
@@ -1,332 +0,0 @@
-{
- "id": "tenant1:app1",
- "internal": true,
- "deploymentIssueId": "321",
- "deploymentSpecField": "<deployment version='1.0'>\n <test />\n <!--<staging />-->\n <prod>\n <region>us-east-3</region>\n <region>us-west-1</region>\n </prod>\n</deployment>\n",
- "validationOverrides": "<validation-overrides>\n <allow until=\"2016-04-28\" comment=\"Renaming content cluster\">content-cluster-removal</allow>\n <allow until=\"2016-08-22\" comment=\"Migrating us-east-3 to C-2E\">cluster-size-reduction</allow>\n <allow until=\"2017-06-30\" comment=\"Test Vespa upgrade tests\">force-automatic-tenant-upgrade-test</allow>\n</validation-overrides>\n",
- "projectId": 102889,
- "deployingField": {
- "build": 42
- },
- "outstandingChangeField": false,
- "queryQuality": 100,
- "writeQuality": 99.99894341115082,
- "pemDeployKeys": [
- "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----"
- ],
- "pemDeveloperKeys": [
- {
- "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----",
- "user": "joe@dev"
- }
- ],
- "instances": [
- {
- "instanceName": "default",
- "deployments": [
- {
- "zone": {
- "environment": "prod",
- "region": "us-west-1"
- },
- "version": "6.173.62",
- "deployTime": 1510837817704,
- "applicationPackageRevision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "clusterInfo": {
- "cluster1": {
- "flavor": "d-3-16-100",
- "cost": 9,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "container",
- "hostnames": [
- "node1",
- "node2"
- ]
- },
- "cluster2": {
- "flavor": "d-12-64-400",
- "cost": 38,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node3",
- "node4",
- "node5"
- ]
- },
- "cluster3": {
- "flavor": "d-12-64-400",
- "cost": 38,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node6",
- "node7",
- "node8",
- "node9"
- ]
- }
- },
- "clusterUtils": {
- "cluster1": {
- "cpu": 0.1720353499228221,
- "mem": 0.4986146831512451,
- "disk": 0.0617671330041831,
- "diskbusy": 0
- },
- "cluster2": {
- "cpu": 0.07505730001866318,
- "mem": 0.7936344432830811,
- "disk": 0.2260549694485994,
- "diskbusy": 0
- },
- "cluster3": {
- "cpu": 0.01712671480989384,
- "mem": 0.0225852754983035,
- "disk": 0.006084436856721915,
- "diskbusy": 0
- }
- },
- "metrics": {
- "queriesPerSecond": 1.25,
- "writesPerSecond": 43.83199977874756,
- "documentCount": 525880277.9999999,
- "queryLatencyMillis": 5.607503938674927,
- "writeLatencyMillis": 20.57866265104621
- }
- },
- {
- "zone": {
- "environment": "test",
- "region": "us-east-1"
- },
- "version": "6.173.62",
- "deployTime": 1511256872316,
- "applicationPackageRevision": {
- "applicationPackageHash": "ec548fa61cbfab7a270a51d46b1263ec1be5d9a8",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "234f3e4e77049d0b9538c9e1b356d17eb1dedb6a"
- }
- },
- "clusterInfo": {},
- "clusterUtils": {},
- "metrics": {
- "queriesPerSecond": 0,
- "writesPerSecond": 0,
- "documentCount": 0,
- "queryLatencyMillis": 0,
- "writeLatencyMillis": 0
- }
- },
- {
- "zone": {
- "environment": "dev",
- "region": "us-east-1"
- },
- "version": "6.173.62",
- "deployTime": 1510597489464,
- "applicationPackageRevision": {
- "applicationPackageHash": "59b883f263c2a3c23dfab249730097d7e0e1ed32"
- },
- "clusterInfo": {
- "cluster1": {
- "flavor": "d-2-8-50",
- "cost": 5,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "container",
- "hostnames": [
- "node1"
- ]
- },
- "cluster2": {
- "flavor": "d-2-8-50",
- "cost": 5,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node2"
- ]
- },
- "cluster3": {
- "flavor": "d-2-8-50",
- "cost": 5,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node3"
- ]
- }
- },
- "clusterUtils": {
- "cluster1": {
- "cpu": 0.191833330678661,
- "mem": 0.4625738318415235,
- "disk": 0.05582004563850269,
- "diskbusy": 0
- },
- "cluster2": {
- "cpu": 0.2227037978608054,
- "mem": 0.2051752598416401,
- "disk": 0.05471533698695047,
- "diskbusy": 0
- },
- "cluster3": {
- "cpu": 0.1869410834020498,
- "mem": 0.1691722576000564,
- "disk": 0.04977374774258153,
- "diskbusy": 0
- }
- },
- "metrics": {
- "queriesPerSecond": 0,
- "writesPerSecond": 0,
- "documentCount": 30916,
- "queryLatencyMillis": 0,
- "writeLatencyMillis": 0
- }
- },
- {
- "zone": {
- "environment": "prod",
- "region": "us-east-3"
- },
- "version": "6.173.62",
- "deployTime": 1510817190016,
- "applicationPackageRevision": {
- "applicationPackageHash": "9db423e1021d7b452d37ec6372bc757d9c1bda87",
- "sourceRevision": {
- "repositoryField": "git@git.host:user/repo.git",
- "branchField": "origin/master",
- "commitField": "49cd7bbb1ed9f4b922083cb042590b0885ffe22b"
- }
- },
- "clusterInfo": {
- "cluster1": {
- "flavor": "d-3-16-100",
- "cost": 9,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "container",
- "hostnames": [
- "node1",
- "node2"
- ]
- },
- "cluster2": {
- "flavor": "d-12-64-400",
- "cost": 38,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node1",
- "node2",
- "node3"
- ]
- },
- "cluster3": {
- "flavor": "d-12-64-400",
- "cost": 38,
- "flavorCpu": 0,
- "flavorMem": 0,
- "flavorDisk": 0,
- "clusterType": "content",
- "hostnames": [
- "node1",
- "node2",
- "node3",
- "node4"
- ]
- }
- },
- "clusterUtils": {
- "cluster1": {
- "cpu": 0.2295038983007097,
- "mem": 0.4627357390237263,
- "disk": 0.05559941525894966,
- "diskbusy": 0
- },
- "cluster2": {
- "cpu": 0.05340429087579549,
- "mem": 0.8107630891552372,
- "disk": 0.226444914138854,
- "diskbusy": 0
- },
- "cluster3": {
- "cpu": 0.02148227413975218,
- "mem": 0.02162174219104161,
- "disk": 0.006057760545243265,
- "diskbusy": 0
- }
- },
- "metrics": {
- "queriesPerSecond": 1.734000012278557,
- "writesPerSecond": 44.59999895095825,
- "documentCount": 525868193.9999999,
- "queryLatencyMillis": 5.65284947195106,
- "writeLatencyMillis": 17.34593812832452
- }
- }
- ],
- "deploymentJobs": {
- "jobStatus": [
- {
- "jobType": "staging.zone",
- "pausedUntil": 321
- }
- ]
- },
- "assignedRotations": [
- {
- "rotationId": "rotation-foo",
- "clusterId": "qrs",
- "endpointId": "default"
- }
- ],
- "rotationStatus2": [
- {
- "rotationId": "rotation-foo",
- "status": [
- {
- "environment": "prod",
- "region": "us-east-3",
- "state": "in"
- }
- ]
- }
- ]
- },
- {
- "instanceName": "empty-instance",
- "deployments": [],
- "deploymentJobs": {
- "jobStatus": []
- },
- "assignedRotations": [],
- "rotationStatus2": []
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/logs.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/logs.json
deleted file mode 100644
index ce9bd2139c7..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/logs.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "deployReal":
- [
- {
- "id": 0,
- "type": "info",
- "at": 0,
- "message": "First"
- },
- {
- "id": 2,
- "type": "debug",
- "at": 1000,
- "message": "Third"
- }
- ],
- "deployTester":
- [
- {
- "id": 1,
- "type": "info",
- "at": 0,
- "message": "Second"
- },
- {
- "id": 3,
- "type": "warning",
- "at": 2000,
- "message": "Fourth"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/run-status.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/run-status.json
deleted file mode 100644
index 618a7e66c5e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/persistence/testdata/run-status.json
+++ /dev/null
@@ -1,67 +0,0 @@
-[
- {
- "id": "tenant:application:default",
- "type": "prod.us-east-3",
- "number": 112358,
- "start": 1196676930000,
- "sleepUntil": 1196676930100,
- "status": "running",
- "lastTestRecord": 3,
- "lastVespaLogTimestamp": 1196676930000432,
- "noNodesDownSince": 321321321321,
- "convergenceSummaryV2": [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233],
- "testerCertificate": "-----BEGIN CERTIFICATE-----\nMIIBEzCBu6ADAgECAgEBMAoGCCqGSM49BAMEMBQxEjAQBgNVBAMTCW15c2Vydmlj\nZTAeFw0xOTA5MDYwNzM3MDZaFw0xOTA5MDcwNzM3MDZaMBQxEjAQBgNVBAMTCW15\nc2VydmljZTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABM0JhD8fV2DlAkjQOGX3\nY50ryMBr3g2+v/uFiRoxJ1muuSOWYrW7HCQIGuzc04fa0QwtaX/voAZKCV51t6jF\n0fwwCgYIKoZIzj0EAwQDRwAwRAIgVbQ3Co1H4X0gmRrtXSyTU0HgBQu9PXHMmX20\n5MyyPSoCIBltOcmaPfdN03L3zqbqZ6PgUBWsvAHgiBzL3hrtJ+iy\n-----END CERTIFICATE-----",
- "steps": {
- "deployInitialReal": "unfinished",
- "installInitialReal": "failed",
- "deployReal": "succeeded",
- "installReal": "unfinished",
- "deactivateReal": "failed",
- "deployTester": "succeeded",
- "installTester": "unfinished",
- "deactivateTester": "failed",
- "copyVespaLogs": "succeeded",
- "startStagingSetup": "succeeded",
- "endStagingSetup": "unfinished",
- "startTests": "succeeded",
- "endTests": "unfinished",
- "report": "failed"
- },
- "stepDetails": {
- "installInitialReal": {
- "startTime": 1196676940000
- },
- "installTester": {
- "startTime": 1196677940000
- }
- },
- "versions": {
- "platform": "1.2.3",
- "repository": "git@github.com:user/repo.git",
- "branch": "master",
- "commit": "f00bad",
- "build": 123,
- "deployedDirectly": true,
- "authorEmail": "a@b",
- "compileVersion": "6.3.1",
- "buildTime": 100,
- "source": {
- "platform": "1.2.2",
- "repository": "git@github.com:user/repo.git",
- "branch": "master",
- "commit": "badb17",
- "build": 122,
- "deployedDirectly": false
- }
- },
- "reason": "because",
- "dependent": {
- "id": "tenant:application:default",
- "type": "prod.us-east-3"
- },
- "change": {
- "platform": "1.2.3",
- "build": 122
- }
- }
-] \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImplTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImplTest.java
deleted file mode 100644
index 313485677ef..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ConfigServerRestExecutorImplTest.java
+++ /dev/null
@@ -1,120 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.proxy;
-
-import ai.vespa.http.HttpURL.Path;
-import ai.vespa.util.http.hc4.SslConnectionSocketFactory;
-import com.github.tomakehurst.wiremock.junit5.WireMockExtension;
-import com.github.tomakehurst.wiremock.stubbing.Scenario;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.yolean.concurrent.Sleeper;
-import org.apache.http.protocol.HttpContext;
-import org.apache.http.protocol.HttpCoreContext;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.RegisterExtension;
-
-import java.io.ByteArrayOutputStream;
-import java.net.URI;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
-
-import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.get;
-import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
-import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class ConfigServerRestExecutorImplTest {
-
- @RegisterExtension
- public final WireMockExtension wireMock = WireMockExtension.newInstance().options(options().dynamicPort()).failOnUnmatchedRequests(true).build();
-
- @Test
- void proxy_with_retries() throws Exception {
- var connectionReuseStrategy = new CountingConnectionReuseStrategy(Set.of("127.0.0.1"));
- var proxy = new ConfigServerRestExecutorImpl(SslConnectionSocketFactory.of(), Sleeper.NOOP, connectionReuseStrategy);
-
- URI url = url();
- String path = url.getPath();
- stubRequests(path);
-
- HttpRequest request = HttpRequest.createTestRequest(url.toString(), com.yahoo.jdisc.http.HttpRequest.Method.GET);
- ProxyRequest proxyRequest = ProxyRequest.tryOne(url, Path.parse(path), request);
-
- // Request is retried
- HttpResponse response = proxy.handle(proxyRequest);
- wireMock.verify(3, getRequestedFor(urlEqualTo(path)));
- assertEquals(200, response.getStatus());
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- response.render(out);
- assertEquals("OK", out.toString());
-
- // No connections are reused as host is a VIP
- assertEquals(0, connectionReuseStrategy.reusedConnections.get(url.getHost()).intValue());
- }
-
- @Test
- void proxy_without_connection_reuse() throws Exception {
- var connectionReuseStrategy = new CountingConnectionReuseStrategy(Set.of());
- var proxy = new ConfigServerRestExecutorImpl(SslConnectionSocketFactory.of(), Sleeper.NOOP, connectionReuseStrategy);
- URI url = url();
- String path = url.getPath();
- stubRequests(path);
-
- HttpRequest request = HttpRequest.createTestRequest(url.toString(), com.yahoo.jdisc.http.HttpRequest.Method.GET);
- ProxyRequest proxyRequest = ProxyRequest.tryOne(url, Path.parse(path), request);
-
- // Connections are reused
- assertEquals(200, proxy.handle(proxyRequest).getStatus());
- assertEquals(3, connectionReuseStrategy.reusedConnections.get(url.getHost()).intValue());
- }
-
- private URI url() {
- return URI.create("http://127.0.0.1:" + wireMock.getPort() + "/");
- }
-
- private void stubRequests(String path) {
- String retryScenario = "Retry scenario";
- String retryRequest = "Retry request 1";
- String retryRequestAgain = "Retry request 2";
-
- wireMock.stubFor(get(urlEqualTo(path)).inScenario(retryScenario)
- .whenScenarioStateIs(Scenario.STARTED)
- .willSetStateTo(retryRequest)
- .willReturn(aResponse().withStatus(500)));
-
- wireMock.stubFor(get(urlEqualTo(path)).inScenario(retryScenario)
- .whenScenarioStateIs(retryRequest)
- .willSetStateTo(retryRequestAgain)
- .willReturn(aResponse().withStatus(500)));
-
- wireMock.stubFor(get(urlEqualTo(path)).inScenario(retryScenario)
- .whenScenarioStateIs(retryRequestAgain)
- .willReturn(aResponse().withBody("OK")));
- }
-
- private static class CountingConnectionReuseStrategy extends ConfigServerRestExecutorImpl.ConnectionReuseStrategy {
-
- private final Map<String, Integer> reusedConnections = new HashMap<>();
-
- public CountingConnectionReuseStrategy(Set<String> vips) {
- super(vips);
- }
-
- @Override
- public boolean keepAlive(org.apache.http.HttpResponse response, HttpContext context) {
- boolean keepAlive = super.keepAlive(response, context);
- String host = HttpCoreContext.adapt(context).getTargetHost().getHostName();
- reusedConnections.putIfAbsent(host, 0);
- if (keepAlive) reusedConnections.compute(host, (ignored, count) -> ++count);
- return keepAlive;
- }
-
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java
deleted file mode 100644
index 87207f5c080..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyRequestTest.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.proxy;
-
-import ai.vespa.http.HttpURL.Path;
-import com.yahoo.jdisc.http.HttpRequest;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-/**
- * @author Haakon Dybdahl
- */
-public class ProxyRequestTest {
-
- @Test
- void testBadUri() {
- assertEquals("Request path '/path' does not end with proxy path '/zone/v2/'",
- assertThrows(IllegalArgumentException.class,
- () -> testRequest("http://domain.tld/path", "/zone/v2/")).getMessage());
- }
-
- @Test
- void testUris() {
- {
- // Root request
- ProxyRequest request = testRequest("http://controller.domain.tld/my/path", "");
- assertEquals(URI.create("http://controller.domain.tld/my/path/"), request.getControllerPrefixUri());
- assertEquals(URI.create("https://cfg.prod.us-north-1.domain.tld:1234"),
- request.createConfigServerRequestUri(URI.create("https://cfg.prod.us-north-1.domain.tld:1234/")));
- }
-
- {
- // Root request with trailing /
- ProxyRequest request = testRequest("http://controller.domain.tld/my/path/", "/");
- assertEquals(URI.create("http://controller.domain.tld/my/path/"), request.getControllerPrefixUri());
- assertEquals(URI.create("https://cfg.prod.us-north-1.domain.tld:1234/"),
- request.createConfigServerRequestUri(URI.create("https://cfg.prod.us-north-1.domain.tld:1234/")));
- }
-
- {
- // API path test
- ProxyRequest request = testRequest("http://controller.domain.tld:1234/my/path/nodes/v2", "/nodes/v2");
- assertEquals(URI.create("http://controller.domain.tld:1234/my/path/"), request.getControllerPrefixUri());
- assertEquals(URI.create("https://cfg.prod.us-north-1.domain.tld/nodes/v2"),
- request.createConfigServerRequestUri(URI.create("https://cfg.prod.us-north-1.domain.tld")));
- }
-
- {
- // API path test with query
- ProxyRequest request = testRequest("http://controller.domain.tld:1234/my/path/nodes/v2/?some=thing", "/nodes/v2/");
- assertEquals(URI.create("http://controller.domain.tld:1234/my/path/"), request.getControllerPrefixUri());
- assertEquals(URI.create("https://cfg.prod.us-north-1.domain.tld/nodes/v2/?some=thing"),
- request.createConfigServerRequestUri(URI.create("https://cfg.prod.us-north-1.domain.tld")));
- }
- }
-
- private static ProxyRequest testRequest(String url, String pathPrefix) {
- return new ProxyRequest(HttpRequest.Method.GET, URI.create(url), Map.of(), null,
- List.of(URI.create("http://example.com")), Path.parse(pathPrefix));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java
deleted file mode 100644
index 49a1abfe0f9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/proxy/ProxyResponseTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.proxy;
-
-import ai.vespa.http.HttpURL.Path;
-import com.yahoo.jdisc.http.HttpRequest;
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayOutputStream;
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author Haakon Dybdahl
- */
-public class ProxyResponseTest {
-
- @Test
- void testRewriteUrl() throws Exception {
- ProxyRequest request = new ProxyRequest(HttpRequest.Method.GET, URI.create("http://domain.tld/zone/v2/dev/us-north-1/configserver"),
- Map.of(), null, List.of(URI.create("http://example.com")), Path.parse("configserver"));
- ProxyResponse proxyResponse = new ProxyResponse(
- request,
- "response link is http://configserver:4443/bla/bla/",
- 200,
- URI.create("http://configserver:1234"),
- "application/json");
-
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-
- proxyResponse.render(outputStream);
- String document = outputStream.toString(StandardCharsets.UTF_8);
- assertEquals("response link is http://domain.tld/zone/v2/dev/us-north-1/bla/bla/", document);
- }
-
- @Test
- void testRewriteSecureUrl() throws Exception {
- ProxyRequest request = new ProxyRequest(HttpRequest.Method.GET, URI.create("https://domain.tld/zone/v2/prod/eu-south-3/configserver"),
- Map.of(), null, List.of(URI.create("http://example.com")), Path.parse("configserver"));
- ProxyResponse proxyResponse = new ProxyResponse(
- request,
- "response link is http://configserver:4443/bla/bla/",
- 200,
- URI.create("http://configserver:1234"),
- "application/json");
-
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-
- proxyResponse.render(outputStream);
- String document = outputStream.toString(StandardCharsets.UTF_8);
- assertEquals("response link is https://domain.tld/zone/v2/prod/eu-south-3/bla/bla/", document);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java
deleted file mode 100644
index 54a592ca070..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ApplicationRequestToDiscFilterRequestWrapper.java
+++ /dev/null
@@ -1,148 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.jdisc.http.HttpRequest;
-import com.yahoo.jdisc.http.filter.DiscFilterRequest;
-
-import java.net.URI;
-import java.security.Principal;
-import java.security.cert.X509Certificate;
-import java.util.Collections;
-import java.util.Enumeration;
-import java.util.List;
-import java.util.Map;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.doNothing;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-
-/**
- * Wraps an {@link Request} into a {@link DiscFilterRequest}. Only a few methods are supported.
- * Changes are not propagated; updated request instance must be retrieved through {@link #getUpdatedRequest()}.
- *
- * @author bjorncs
- */
-public class ApplicationRequestToDiscFilterRequestWrapper extends DiscFilterRequest {
-
- private final Request request;
- private final List<X509Certificate> clientCertificateChain;
- private Principal userPrincipal;
-
- public ApplicationRequestToDiscFilterRequestWrapper(Request request) {
- this(request, Collections.emptyList());
- }
-
- public ApplicationRequestToDiscFilterRequestWrapper(Request request, List<X509Certificate> clientCertificateChain) {
- super(createDummyHttpRequest(request));
- this.request = request;
- this.userPrincipal = request.getUserPrincipal().orElse(null);
- this.clientCertificateChain = clientCertificateChain;
- }
-
- private static HttpRequest createDummyHttpRequest(Request req) {
- HttpRequest dummy = mock(HttpRequest.class, invocation -> { throw new UnsupportedOperationException(); });
- doReturn(URI.create(req.getUri()).normalize()).when(dummy).getUri();
- doNothing().when(dummy).copyHeaders(any());
- doReturn(Map.of()).when(dummy).parameters();
- return dummy;
- }
-
- public Request getUpdatedRequest() {
- Request updatedRequest = new Request(this.request.getUri(), this.request.getBody(), this.request.getMethod(), this.userPrincipal);
- this.request.getHeaders().forEach(updatedRequest.getHeaders()::put);
- updatedRequest.getAttributes().putAll(this.request.getAttributes());
- return updatedRequest;
- }
-
- @Override
- public String getMethod() {
- return request.getMethod().name();
- }
-
- @Override
- public String getParameter(String name) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Enumeration<String> getParameterNames() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void addHeader(String name, String value) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public String getHeader(String name) {
- return request.getHeaders().getFirst(name);
- }
-
- @Override
- public Enumeration<String> getHeaderNames() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public List<String> getHeaderNamesAsList() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Enumeration<String> getHeaders(String name) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public List<String> getHeadersAsList(String name) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void removeHeaders(String name) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void setHeaders(String name, String value) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void setHeaders(String name, List<String> values) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Principal getUserPrincipal() {
- return this.userPrincipal;
- }
-
- @Override
- public void setUserPrincipal(Principal principal) {
- this.userPrincipal = principal;
- }
-
- @Override
- public List<X509Certificate> getClientCertificateChain() {
- return clientCertificateChain;
- }
-
- @Override
- public void clearCookies() {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public Object getAttribute(String name) {
- return request.getAttributes().get(name);
- }
-
- @Override
- public void setAttribute(String name, Object value) {
- request.getAttributes().put(name, value);
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java
deleted file mode 100644
index f2826bad5b5..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ContainerTester.java
+++ /dev/null
@@ -1,236 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi;
-
-import com.yahoo.application.container.JDisc;
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.application.container.handler.Response;
-import com.yahoo.component.ComponentSpecification;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.container.http.filter.FilterChainRepository;
-import com.yahoo.jdisc.http.filter.SecurityRequestFilter;
-import com.yahoo.jdisc.http.filter.SecurityRequestFilterChain;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock;
-import com.yahoo.vespa.hosted.controller.api.integration.stubs.MockUserManagement;
-import com.yahoo.vespa.hosted.controller.integration.ServiceRegistryMock;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.charset.CharacterCodingException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.Optional;
-import java.util.function.Supplier;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * Provides testing of JSON container responses
- *
- * @author bratseth
- */
-public class ContainerTester {
-
- private static final boolean writeResponses = false;
-
- private final JDisc container;
- private final String responseFilePath;
-
- public ContainerTester(JDisc container, String responseFilePath) {
- this.container = container;
- this.responseFilePath = responseFilePath;
- }
-
- public Controller controller() {
- return (Controller) container.components().getComponent(Controller.class.getName());
- }
-
- public AthenzClientFactoryMock athenzClientFactory() {
- return (AthenzClientFactoryMock) container.components().getComponent(AthenzClientFactoryMock.class.getName());
- }
-
- public InMemoryFlagSource flagSource() {
- return (InMemoryFlagSource) container.components().getComponent(InMemoryFlagSource.class.getName());
- }
-
- public ServiceRegistryMock serviceRegistry() {
- return (ServiceRegistryMock) container.components().getComponent(ServiceRegistryMock.class.getName());
- }
-
- public MockUserManagement userManagement() {
- return (MockUserManagement) container.components().getComponent(MockUserManagement.class.getName());
- }
-
- public void authorize(AthenzDomain tenantDomain, AthenzIdentity identity, ApplicationAction action, ApplicationName application) {
- athenzClientFactory().getSetup()
- .domains.get(tenantDomain)
- .applications.get(new com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId(application.value()))
- .addRoleMember(action, identity);
- }
-
- public void assertJsonResponse(Supplier<Request> request, File responseFile) {
- assertResponse(request.get(), responseFile, 200, false, true);
- }
-
- public void assertResponse(Supplier<Request> request, File responseFile) {
- assertResponse(request.get(), responseFile);
- }
-
- public void assertResponse(Request request, File responseFile) {
- assertResponse(request, responseFile, 200);
- }
-
- public void assertResponse(Supplier<Request> request, File responseFile, int expectedStatusCode) {
- assertResponse(request.get(), responseFile, expectedStatusCode);
- }
-
- public void assertResponse(Request request, File responseFile, int expectedStatusCode) {
- assertResponse(request, responseFile, expectedStatusCode, true);
- }
-
- public void assertResponse(Request request, File responseFile, int expectedStatusCode, boolean removeWhitespace) {
- assertResponse(request, responseFile, expectedStatusCode, removeWhitespace, false);
- }
-
- private void assertResponse(Request request, File responseFile, int expectedStatusCode, boolean removeWhitespace, boolean compareJson) {
- String expectedResponse = readTestFile(responseFile.toString());
- expectedResponse = include(expectedResponse);
- FilterResult filterResult = invokeSecurityFilters(request);
- request = filterResult.request;
- Response response = filterResult.response != null ? filterResult.response : container.handleRequest(request);
- String responseString;
- try {
- responseString = response.getBodyAsString();
- }
- catch (CharacterCodingException e) {
- throw new UncheckedIOException(e);
- }
- try {
- if (responseFile.toString().endsWith(".json")) {
- byte[] expected = SlimeUtils.toJsonBytes(SlimeUtils.jsonToSlimeOrThrow(expectedResponse).get(), false);
- byte[] actual = SlimeUtils.toJsonBytes(SlimeUtils.jsonToSlimeOrThrow(responseString).get(), false);
- if (writeResponses) writeTestFile(responseFile.toString(), actual);
- else assertEquals(new String(expected, UTF_8), new String(actual, UTF_8));
- }
- else { // Not JSON? Let's do a verbatim comparison, then ...
- if (writeResponses) writeTestFile(responseFile.toString(), responseString.getBytes(UTF_8));
- else assertEquals(expectedResponse, responseString);
- }
- }
- catch (IOException e) {
- fail("failed writing JSON: " + e);
- }
- assertEquals(expectedStatusCode, response.getStatus(), "Status code");
- }
-
- public void assertResponse(Supplier<Request> request, String expectedResponse) {
- assertResponse(request, expectedResponse, 200);
- }
-
- public void assertResponse(Request request, String expectedResponse) {
- assertResponse(() -> request, expectedResponse, 200);
- }
-
- public void assertResponse(Request request, String expectedResponse, int expectedStatusCode) {
- assertResponse(() -> request, expectedResponse, expectedStatusCode);
- }
-
- public void assertJsonResponse(Supplier<Request> request, String expectedResponse, int expectedStatusCode) {
- assertResponse(request,
- (response) -> assertEquals(SlimeUtils.toJson(SlimeUtils.jsonToSlimeOrThrow(expectedResponse).get(), false),
- SlimeUtils.toJson(SlimeUtils.jsonToSlimeOrThrow(response.getBodyAsString()).get(), false)),
- expectedStatusCode);
- }
-
- public void assertResponse(Supplier<Request> request, String expectedResponse, int expectedStatusCode) {
- assertResponse(request,
- (response) -> assertEquals(expectedResponse, new String(response.getBody(), UTF_8)),
- expectedStatusCode);
- }
-
- public void assertResponse(Supplier<Request> requestSupplier, ConsumerThrowingException<Response> responseAssertion, int expectedStatusCode) {
- var request = requestSupplier.get();
- FilterResult filterResult = invokeSecurityFilters(request);
- request = filterResult.request;
- Response response = filterResult.response != null ? filterResult.response : container.handleRequest(request);
- try {
- responseAssertion.accept(response);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- assertEquals(expectedStatusCode, response.getStatus(), "Status code");
- }
-
- // Hack to run request filters as part of the request processing chain.
- // Limitation: Bindings ignored, disc filter request wrapper only support limited set of methods.
- private FilterResult invokeSecurityFilters(Request request) {
- FilterChainRepository filterChainRepository = (FilterChainRepository) container.components().getComponent(FilterChainRepository.class.getName());
- SecurityRequestFilterChain chain = (SecurityRequestFilterChain) filterChainRepository.getFilter(ComponentSpecification.fromString("default"));
- for (SecurityRequestFilter securityRequestFilter : chain.getFilters()) {
- ApplicationRequestToDiscFilterRequestWrapper discFilterRequest = new ApplicationRequestToDiscFilterRequestWrapper(request);
- ResponseHandlerToApplicationResponseWrapper responseHandlerWrapper = new ResponseHandlerToApplicationResponseWrapper();
- securityRequestFilter.filter(discFilterRequest, responseHandlerWrapper);
- request = discFilterRequest.getUpdatedRequest();
- Optional<Response> filterResponse = responseHandlerWrapper.toResponse();
- if (filterResponse.isPresent()) {
- return new FilterResult(request, filterResponse.get());
- }
- }
- return new FilterResult(request, null);
- }
-
- /** Replaces @include(localFile) with the content of the file */
- private String include(String response) {
- // Please don't look at this code
- int includeIndex = response.indexOf("@include(");
- if (includeIndex < 0) return response;
- String prefix = response.substring(0, includeIndex);
- String rest = response.substring(includeIndex + "@include(".length());
- int filenameEnd = rest.indexOf(")");
- String includeFileName = rest.substring(0, filenameEnd);
- String includedContent = readTestFile(includeFileName);
- includedContent = include(includedContent);
- String postFix = rest.substring(filenameEnd + 1);
- postFix = include(postFix);
- return prefix + includedContent + postFix;
- }
-
- private void writeTestFile(String name, byte[] content) {
- try {
- Files.write(Paths.get(responseFilePath, name), content);
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- private String readTestFile(String name) {
- try {
- return new String(Files.readAllBytes(Paths.get(responseFilePath, name)));
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- private static class FilterResult {
- final Request request;
- final Response response;
-
- FilterResult(Request request, Response response) {
- this.request = request;
- this.response = response;
- }
- }
-
- @FunctionalInterface
- public interface ConsumerThrowingException<T> {
- void accept(T t) throws Exception;
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
deleted file mode 100644
index 06c2a03d98a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerCloudTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi;
-
-import ai.vespa.hosted.api.MultiPartStreamer;
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.jdisc.http.filter.security.misc.User;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.yolean.Exceptions;
-
-import java.nio.charset.StandardCharsets;
-import java.security.Principal;
-import java.util.Set;
-import java.util.function.Supplier;
-
-/**
- * Controller container test with services.xml which accommodates cloud user management.
- *
- * @author jonmv
- */
-public class ControllerContainerCloudTest extends ControllerContainerTest {
-
- @Override
- protected SystemName system() {
- return SystemName.Public;
- }
-
- @Override
- protected String variablePartXml() {
- return """
- <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControlRequests'/>
- <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControl'/>
-
- <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>
- <binding>http://localhost/application/v4/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v1.ZoneApiHandler'>
- <binding>http://localhost/zone/v1</binding>
- <binding>http://localhost/zone/v1/*</binding>
- </handler>
-
- <http>
- <server id='default' port='8080' />
- <filtering>
- <request-chain id='default'>
- <filter id='com.yahoo.vespa.hosted.controller.restapi.filter.ControllerAuthorizationFilter'/>
- <binding>http://localhost/*</binding>
- </request-chain>
- </filtering>
- </http>
- """;
- }
-
- protected static final String accessDenied = """
- {
- "code" : 403,
- "message" : "Access denied"
- }""";
-
- protected RequestBuilder request(String path) { return new RequestBuilder(path, Request.Method.GET); }
- protected RequestBuilder request(String path, Request.Method method) { return new RequestBuilder(path, method); }
-
- protected static class RequestBuilder implements Supplier<Request> {
- private final String path;
- private final Request.Method method;
- private byte[] data = new byte[0];
- private Principal principal = () -> "user@test";
- private User user;
- private Set<Role> roles = Set.of(Role.everyone());
- private String contentType;
-
- private RequestBuilder(String path, Request.Method method) {
- this.path = path;
- this.method = method;
- }
-
- public RequestBuilder contentType(String contentType) { this.contentType = contentType; return this; }
- public RequestBuilder data(MultiPartStreamer streamer) {
- return Exceptions.uncheck(() -> data(streamer.data().readAllBytes()).contentType(streamer.contentType()));
- }
- public RequestBuilder data(byte[] data) { this.data = data; return this; }
- public RequestBuilder data(String data) { this.data = data.getBytes(StandardCharsets.UTF_8); return this; }
- public RequestBuilder principal(String principal) { this.principal = new SimplePrincipal(principal){ }; return this; }
- public RequestBuilder user(User user) { this.user = user; return this; }
- public RequestBuilder roles(Set<Role> roles) { this.roles = roles; return this; }
- public RequestBuilder roles(Role... roles) { return roles(Set.of(roles)); }
-
- @Override
- public Request get() {
- Request request = new Request("http://localhost:8080" + path, data, method, principal);
- request.getAttributes().put(SecurityContext.ATTRIBUTE_NAME, new SecurityContext(principal, roles));
- if (user != null) {
- request.getAttributes().put(User.ATTRIBUTE_NAME, user);
- }
- request.getHeaders().put("Content-Type", contentType);
- return request;
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
deleted file mode 100644
index 3ada598f4f8..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ControllerContainerTest.java
+++ /dev/null
@@ -1,190 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi;
-
-import com.yahoo.application.Networking;
-import com.yahoo.application.container.JDisc;
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-
-import static com.yahoo.vespa.hosted.controller.integration.AthenzFilterMock.IDENTITY_HEADER_NAME;
-import static com.yahoo.vespa.hosted.controller.integration.AthenzFilterMock.OKTA_ACCESS_TOKEN_HEADER_NAME;
-import static com.yahoo.vespa.hosted.controller.integration.AthenzFilterMock.OKTA_IDENTITY_TOKEN_HEADER_NAME;
-
-/**
- * Superclass of REST API tests which needs to set up a functional container instance.
- *
- * This is a test superclass, not a tester because we need the start and stop methods.
- *
- * DO NOT ADD ANYTHING HERE: If you need additional fields and methods, create a tester
- * which gets the container instance at construction time (in the test method) instead.
- *
- * @author bratseth
- */
-public class ControllerContainerTest {
-
- protected static final AthenzUser hostedOperator = AthenzUser.fromUserId("alice");
- protected static final AthenzUser defaultUser = AthenzUser.fromUserId("bob");
-
- protected JDisc container;
-
- @BeforeEach
- public void startContainer() {
- container = JDisc.fromServicesXml(controllerServicesXml(), networking());
- addUserToHostedOperatorRole(hostedOperator);
- }
-
- protected Networking networking() { return Networking.disable; }
-
- @AfterEach
- public void stopContainer() { container.close(); }
-
- private String controllerServicesXml() {
- return """
- <container version='1.0'>
- <config name="container.handler.threadpool">
- <maxthreads>10</maxthreads>
- </config>
- <config name="cloud.config.configserver">
- <system>%s</system>
- </config>
- <config name="vespa.hosted.rotation.config.rotations">
- <rotations>
- <item key="rotation-id-1">rotation-fqdn-1</item>
- <item key="rotation-id-2">rotation-fqdn-2</item>
- <item key="rotation-id-3">rotation-fqdn-3</item>
- <item key="rotation-id-4">rotation-fqdn-4</item>
- <item key="rotation-id-5">rotation-fqdn-5</item>
- </rotations>
- </config>
- <config name="vespa.hosted.controller.config.core-dump-token-resealing">
- <resealingPrivateKeyName>a.really.cool.key</resealingPrivateKeyName>
- </config>
-
- <accesslog type='disabled'/>
-
- <component id='com.yahoo.vespa.flags.InMemoryFlagSource'/>
- <component id='com.yahoo.vespa.configserver.flags.db.FlagsDbImpl'/>
- <component id='com.yahoo.vespa.curator.mock.MockCurator'/>
- <component id='com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb'/>
- <component id='com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock'/>
- <component id='com.yahoo.vespa.hosted.controller.integration.ServiceRegistryMock'/>
- <component id='com.yahoo.vespa.hosted.controller.Controller'/>
- <component id='com.yahoo.vespa.hosted.controller.integration.ConfigServerProxyMock'/>
- <component id='com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintenance'/>
- <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockMavenRepository'/>
- <component id='com.yahoo.vespa.hosted.controller.api.integration.stubs.MockUserManagement'/>
- <component id='com.yahoo.vespa.hosted.controller.integration.SecretStoreMock'/>
-
- <handler id='com.yahoo.vespa.hosted.controller.restapi.deployment.DeploymentApiHandler'>
- <binding>http://localhost/deployment/v1/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.deployment.BadgeApiHandler'>
- <binding>http://localhost/badge/v1/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.deployment.CliApiHandler'>
- <binding>http://localhost/cli/v1/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.controller.ControllerApiHandler'>
- <binding>http://localhost/controller/v1/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.os.OsApiHandler'>
- <binding>http://localhost/os/v1/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v2.ZoneApiHandler'>
- <binding>http://localhost/zone/v2</binding>
- <binding>http://localhost/zone/v2/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.configserver.ConfigServerApiHandler'>
- <binding>http://localhost/configserver/v1</binding>
- <binding>http://localhost/configserver/v1/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.flags.AuditedFlagsHandler'>
- <binding>http://localhost/flags/v1</binding>
- <binding>http://localhost/flags/v1/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.user.UserApiHandler'>
- <binding>http://localhost/user/v1/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.routing.RoutingApiHandler'>
- <binding>http://localhost/routing/v1/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.changemanagement.ChangeManagementApiHandler'>
- <binding>http://localhost/changemanagement/v1/*</binding>
- </handler>
- %s
- </container>
- """.formatted(system().value(), variablePartXml());
- }
-
- protected SystemName system() {
- return SystemName.main;
- }
-
- protected String variablePartXml() {
- return " <component id='com.yahoo.vespa.hosted.controller.security.AthenzAccessControlRequests'/>\n" +
- " <component id='com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade'/>\n" +
-
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>\n" +
- " <binding>http://localhost/application/v4/*</binding>\n" +
- " </handler>\n" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.athenz.AthenzApiHandler'>\n" +
- " <binding>http://localhost/athenz/v1/*</binding>\n" +
- " </handler>\n" +
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v1.ZoneApiHandler'>\n" +
- " <binding>http://localhost/zone/v1</binding>\n" +
- " <binding>http://localhost/zone/v1/*</binding>\n" +
- " </handler>\n" +
-
- " <http>\n" +
- " <server id='default' port='8080' />\n" +
- " <filtering>\n" +
- " <request-chain id='default'>\n" +
- " <filter id='com.yahoo.vespa.hosted.controller.integration.AthenzFilterMock'/>\n" +
- " <filter id='com.yahoo.vespa.hosted.controller.restapi.filter.AthenzRoleFilter'/>\n" +
- " <filter id='com.yahoo.vespa.hosted.controller.restapi.filter.ControllerAuthorizationFilter'/>\n" +
- " <binding>http://localhost/*</binding>\n" +
- " </request-chain>\n" +
- " </filtering>\n" +
- " </http>\n";
- }
-
- protected static Request authenticatedRequest(String uri) {
- return addIdentityToRequest(new Request(uri), defaultUser);
- }
-
- protected static Request authenticatedRequest(String uri, String body, Request.Method method) {
- return addIdentityToRequest(new Request(uri, body, method), defaultUser);
- }
-
- protected static Request operatorRequest(String uri) {
- return addIdentityToRequest(new Request(uri), hostedOperator);
- }
-
- protected static Request operatorRequest(String uri, String body, Request.Method method) {
- return addIdentityToRequest(new Request(uri, body, method), hostedOperator);
- }
-
- protected static Request addIdentityToRequest(Request request, AthenzIdentity identity) {
- request.getHeaders().put(IDENTITY_HEADER_NAME, identity.getFullName());
- return request;
- }
-
- protected static Request addOAuthCredentials(Request request, OAuthCredentials oAuthCredentials) {
- request.getHeaders().put(OKTA_IDENTITY_TOKEN_HEADER_NAME, oAuthCredentials.idToken());
- request.getHeaders().put(OKTA_ACCESS_TOKEN_HEADER_NAME, oAuthCredentials.accessToken());
- return request;
- }
-
- protected void addUserToHostedOperatorRole(AthenzIdentity athenzIdentity) {
- AthenzClientFactoryMock mock = (AthenzClientFactoryMock) container.components()
- .getComponent(AthenzClientFactoryMock.class.getName());
- mock.getSetup().addHostedOperator(athenzIdentity);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ResponseHandlerToApplicationResponseWrapper.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ResponseHandlerToApplicationResponseWrapper.java
deleted file mode 100644
index 765da006deb..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/ResponseHandlerToApplicationResponseWrapper.java
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi;
-
-import com.yahoo.jdisc.Response;
-import com.yahoo.jdisc.handler.CompletionHandler;
-import com.yahoo.jdisc.handler.ContentChannel;
-import com.yahoo.jdisc.handler.ResponseHandler;
-
-import java.nio.ByteBuffer;
-import java.util.Optional;
-import java.util.Queue;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * A {@link ResponseHandler} that caches the response content and
- * converts a {@link Response} to {@link com.yahoo.application.container.handler.Response}.
- *
- * @author bjorncs
- */
-public class ResponseHandlerToApplicationResponseWrapper implements ResponseHandler {
-
- private Response response;
- private SimpleContentChannel contentChannel;
-
- @Override
- public ContentChannel handleResponse(Response response) {
- this.response = response;
- SimpleContentChannel contentChannel = new SimpleContentChannel();
- this.contentChannel = contentChannel;
- return contentChannel;
- }
-
- public Optional<com.yahoo.application.container.handler.Response> toResponse() {
- return Optional.ofNullable(this.response)
- .map(r -> {
- byte[] bytes = contentChannel.toByteArray();
- return new com.yahoo.application.container.handler.Response(response.getStatus(), bytes);
- });
- }
-
- private static class SimpleContentChannel implements ContentChannel {
-
- private final Queue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>();
- private final AtomicBoolean closed = new AtomicBoolean(false);
-
- @Override
- public void write(ByteBuffer buf, CompletionHandler handler) {
- buffers.add(buf);
- handler.completed();
- }
-
- @Override
- public void close(CompletionHandler handler) {
- handler.completed();
- if (closed.getAndSet(true)) {
- throw new IllegalStateException("Already closed");
- }
- }
-
- byte[] toByteArray() {
- if (!closed.get()) {
- throw new IllegalStateException("Content channel not closed yet");
- }
- int totalSize = 0;
- for (ByteBuffer responseBuffer : buffers) {
- totalSize += responseBuffer.remaining();
- }
- ByteBuffer totalBuffer = ByteBuffer.allocate(totalSize);
- for (ByteBuffer responseBuffer : buffers) {
- totalBuffer.put(responseBuffer);
- }
- return totalBuffer.array();
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
deleted file mode 100644
index eb1885423b1..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiCloudTest.java
+++ /dev/null
@@ -1,858 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import ai.vespa.hosted.api.MultiPartStreamer;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.restapi.RestApiException;
-import com.yahoo.slime.Cursor;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.FingerPrint;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.secrets.TenantSecretStore;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest;
-import com.yahoo.vespa.hosted.controller.security.Auth0Credentials;
-import com.yahoo.vespa.hosted.controller.security.CloudTenantSpec;
-import com.yahoo.vespa.hosted.controller.security.Credentials;
-import com.yahoo.vespa.hosted.controller.tenant.CloudAccountInfo;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicReference;
-
-import static com.yahoo.application.container.handler.Request.Method.DELETE;
-import static com.yahoo.application.container.handler.Request.Method.GET;
-import static com.yahoo.application.container.handler.Request.Method.POST;
-import static com.yahoo.application.container.handler.Request.Method.PUT;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author oyving
- */
-public class ApplicationApiCloudTest extends ControllerContainerCloudTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/";
-
- private ContainerTester tester;
-
- private static final TenantName tenantName = TenantName.from("scoober");
- private static final ApplicationName applicationName = ApplicationName.from("albums");
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, responseFiles);
- ((InMemoryFlagSource) tester.controller().flagSource())
- .withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true);
- setupTenantAndApplication();
- }
-
- @Test
- void tenant_info_profile() {
- var request = request("/application/v4/tenant/scoober/info/profile", GET)
- .roles(Set.of(Role.reader(tenantName)));
- tester.assertResponse(request, "{\"contact\":{\"name\":\"\",\"email\":\"\",\"emailVerified\":false},\"tenant\":{\"company\":\"\",\"website\":\"\"}}", 200);
-
- var updateRequest = request("/application/v4/tenant/scoober/info/profile", PUT)
- .data("{\"contact\":{\"name\":\"Some Name\",\"email\":\"foo@example.com\"},\"tenant\":{\"company\":\"Scoober, Inc.\",\"website\":\"https://example.com/\"}}")
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200);
-
- tester.assertResponse(request, "{\"contact\":{\"name\":\"Some Name\",\"email\":\"foo@example.com\",\"emailVerified\":false},\"tenant\":{\"company\":\"Scoober, Inc.\",\"website\":\"https://example.com/\"}}", 200);
- }
-
- @Test
- void tenant_info_profile_too_long() {
- var request = request("/application/v4/tenant/scoober/info/profile", PUT)
- .data("{\"contact\":{\"name\":\"" + "a".repeat(513) + "\",\"email\":\"foo@example.com\"},\"tenant\":{\"company\":\"Scoober, Inc.\",\"website\":\"https://example.com/\"}}")
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(request, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Input value too long\"}", 400);
- }
-
- @Test
- void tenant_info_billing() {
- var expectedResponse = """
- {
- "contact": {
- "name":"",
- "email":"",
- "emailVerified":false,
- "phone":""
- },
- "taxId": {
- "country": "",
- "type": "",
- "code": ""
- },
- "purchaseOrder":"",
- "invoiceEmail":"",
- "tosApproval": {
- "at": "",
- "by": ""
- }
- }
- """;
- var request = request("/application/v4/tenant/scoober/info/billing", GET)
- .roles(Set.of(Role.reader(tenantName)));
- tester.assertJsonResponse(request, expectedResponse, 200);
-
- var fullBillingContact = """
- {
- "contact": {
- "name":"name",
- "email":"foo@example",
- "phone":"phone"
- },
- "taxId":{"country": "NO", "type": "no_vat", "code": "123456789MVA"},
- "purchaseOrder":"PO9001",
- "invoiceEmail":"billing@mycomp.any",
- "address": {
- "addressLines":"addressLines",
- "postalCodeOrZip":"postalCodeOrZip",
- "city":"city",
- "stateRegionProvince":"stateRegionProvince",
- "country":"country"
- }
- }
- """;
- var updateRequest = request("/application/v4/tenant/scoober/info/billing", PUT)
- .data(fullBillingContact)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200);
-
- var approveToSRequest = request("/application/v4/tenant/scoober/terms-of-service", POST)
- .data("{}").roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(approveToSRequest, "{\"message\":\"Terms of service approved by user@test\"}", 200);
-
- expectedResponse = """
- {
- "contact": {
- "name":"name",
- "email":"foo@example",
- "emailVerified": false,
- "phone":"phone"
- },
- "taxId": {
- "country": "NO",
- "type": "no_vat",
- "code": "123456789MVA"
- },
- "purchaseOrder":"PO9001",
- "invoiceEmail":"billing@mycomp.any",
- "tosApproval": {
- "at": "2020-09-13T12:26:40Z",
- "by": "user@test"
- },
- "address": {
- "addressLines":"addressLines",
- "postalCodeOrZip":"postalCodeOrZip",
- "city":"city",
- "stateRegionProvince":"stateRegionProvince",
- "country":"country"
- }
- }
- """;
- tester.assertJsonResponse(request, expectedResponse, 200);
-
- var unapproveToSRequest = request("/application/v4/tenant/scoober/terms-of-service", DELETE)
- .data("{}").roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(unapproveToSRequest, "{\"message\":\"Terms of service approval removed\"}", 200);
-
- expectedResponse = """
- {
- "contact": {
- "name":"name",
- "email":"foo@example",
- "emailVerified": false,
- "phone":"phone"
- },
- "taxId": {
- "country": "NO",
- "type": "no_vat",
- "code": "123456789MVA"
- },
- "purchaseOrder":"PO9001",
- "invoiceEmail":"billing@mycomp.any",
- "tosApproval": {
- "at": "",
- "by": ""
- },
- "address": {
- "addressLines":"addressLines",
- "postalCodeOrZip":"postalCodeOrZip",
- "city":"city",
- "stateRegionProvince":"stateRegionProvince",
- "country":"country"
- }
- }
- """;
- tester.assertJsonResponse(request, expectedResponse, 200);
- }
-
- @Test
- void tenant_info_contacts() {
- var request = request("/application/v4/tenant/scoober/info/contacts", GET)
- .roles(Set.of(Role.reader(tenantName)));
- tester.assertResponse(request, "{\"contacts\":[{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"developer@scoober\",\"emailVerified\":true}]}", 200);
-
-
- var fullContacts = """
- {
- "contacts":[
- {
- "audiences":["tenant"]
- ,"email":"contact1@example.com",
- "emailVerified":false
- },
- {
- "audiences":["notifications"],
- "email":"contact2@example.com",
- "emailVerified":false
- },
- {
- "audiences":["tenant","notifications"],
- "email":"contact3@example.com",
- "emailVerified":false
- }
- ]
- }
- """;
- var updateRequest = request("/application/v4/tenant/scoober/info/contacts", PUT)
- .data(fullContacts)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(updateRequest, "{\"message\":\"Tenant info updated\"}", 200);
- tester.assertJsonResponse(request, fullContacts, 200);
- }
-
- @Test
- void tenant_info_workflow() {
- var infoRequest =
- request("/application/v4/tenant/scoober/info", GET)
- .roles(Set.of(Role.reader(tenantName)));
- tester.assertResponse(infoRequest, "{\"name\":\"\",\"email\":\"\",\"website\":\"\",\"contactName\":\"\",\"contactEmail\":\"\",\"contactEmailVerified\":false,\"contacts\":[{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"developer@scoober\",\"emailVerified\":true}]}", 200);
-
- String partialInfo = "{\"contactName\":\"newName\", \"contactEmail\": \"foo@example.com\", \"billingContact\":{\"name\":\"billingName\"}}";
- var postPartial =
- request("/application/v4/tenant/scoober/info", PUT)
- .data(partialInfo)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(postPartial, "{\"message\":\"Tenant info updated\"}", 200);
-
- String partialContacts = "{\"contacts\": [{\"audiences\": [\"tenant\"],\"email\": \"contact1@example.com\"}]}";
- var postPartialContacts =
- request("/application/v4/tenant/scoober/info", PUT)
- .data(partialContacts)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(postPartialContacts, "{\"message\":\"Tenant info updated\"}", 200);
-
- // Read back the updated info
- var expectedResponse = """
- {
- "name":"",
- "email":"",
- "website":"",
- "contactName":"newName",
- "contactEmail":"foo@example.com",
- "contactEmailVerified":false,
- "billingContact": {
- "name":"billingName",
- "email":"","emailVerified":false,
- "phone":"",
- "taxId": {
- "country": "",
- "type": "",
- "code": ""
- },
- "purchaseOrder":"",
- "invoiceEmail":"",
- "tosApproval": {
- "at": "",
- "by": ""
- }
- },
- "contacts": [
- {"audiences":["tenant"],"email":"contact1@example.com","emailVerified":false}
- ]
- }
- """;
- tester.assertJsonResponse(infoRequest, expectedResponse, 200);
-
- var fullInfo = """
- {
- "name":"name",
- "email":"foo@example",
- "website":"https://yahoo.com",
- "contactName":"contactName",
- "contactEmail":"contact@example.com",
- "contactEmailVerified":false,
- "address": {
- "addressLines":"addressLines",
- "postalCodeOrZip":"postalCodeOrZip",
- "city":"city",
- "stateRegionProvince":"stateRegionProvince",
- "country":"country"
- },
- "billingContact": {
- "name":"name",
- "email":"foo@example",
- "emailVerified":false,
- "phone":"phone",
- "taxId": {
- "country": "",
- "type": "",
- "code": ""
- },
- "purchaseOrder":"",
- "invoiceEmail":"",
- "address": {
- "addressLines":"addressLines",
- "postalCodeOrZip":"postalCodeOrZip",
- "city":"city",
- "stateRegionProvince":"stateRegionProvince",
- "country":"country"
- },
- "tosApproval": {
- "at": "",
- "by": ""
- }
- },
- "contacts": [
- {
- "audiences":["tenant"],
- "email":"contact1@example.com",
- "emailVerified":false
- },
- {
- "audiences":["notifications"],
- "email":"contact2@example.com",
- "emailVerified":false
- },
- {
- "audiences":["tenant","notifications"]
- ,"email":"contact3@example.com",
- "emailVerified":false
- }
- ]
- }
- """;
- // Now set all fields
- var postFull =
- request("/application/v4/tenant/scoober/info", PUT)
- .data(fullInfo)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(postFull, "{\"message\":\"Tenant info updated\"}", 200);
-
- // Now compare the updated info with the full info we sent
- tester.assertJsonResponse(infoRequest, fullInfo, 200);
-
- var invalidBody = "{\"mail\":\"contact1@example.com\", \"mailType\":\"blurb\"}";
- var resendMailRequest =
- request("/application/v4/tenant/scoober/info/resend-mail-verification", PUT)
- .data(invalidBody)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(resendMailRequest, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Unknown mail type blurb\"}", 400);
-
- var resendMailBody = "{\"mail\":\"contact2@example.com\", \"mailType\":\"notifications\"}";
- resendMailRequest =
- request("/application/v4/tenant/scoober/info/resend-mail-verification", PUT)
- .data(resendMailBody)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(resendMailRequest, "{\"message\":\"Re-sent verification mail to contact2@example.com\"}", 200);
- }
-
- @Test
- void tenant_info_missing_fields() {
- // tenants can be created with empty tenant info - they're not part of the POST to v4/tenant
- var infoRequest =
- request("/application/v4/tenant/scoober/info", GET)
- .roles(Set.of(Role.reader(tenantName)));
- tester.assertResponse(infoRequest, "{\"name\":\"\",\"email\":\"\",\"website\":\"\",\"contactName\":\"\",\"contactEmail\":\"\",\"contactEmailVerified\":false,\"contacts\":[{\"audiences\":[\"tenant\",\"notifications\"],\"email\":\"developer@scoober\",\"emailVerified\":true}]}", 200);
-
- // name needs to be present and not blank
- var partialInfoMissingName = "{\"contactName\": \" \"}";
- var missingNameResponse = request("/application/v4/tenant/scoober/info", PUT)
- .data(partialInfoMissingName)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(missingNameResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"'contactName' cannot be empty\"}", 400);
-
- // email needs to be present, not blank, and contain an @
- var partialInfoMissingEmail = "{\"contactName\": \"Scoober Rentals Inc.\", \"contactEmail\": \" \"}";
- var missingEmailResponse = request("/application/v4/tenant/scoober/info", PUT)
- .data(partialInfoMissingEmail)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(missingEmailResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid email address\"}", 400);
-
- var partialInfoBadEmail = "{\"contactName\": \"Scoober Rentals Inc.\", \"contactEmail\": \"somethingweird\"}";
- var badEmailResponse = request("/application/v4/tenant/scoober/info", PUT)
- .data(partialInfoBadEmail)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(badEmailResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid email address\"}", 400);
-
- var invalidWebsite = "{\"contactName\": \"Scoober Rentals Inc.\", \"contactEmail\": \"email@scoober.com\", \"website\": \"scoober\" }";
- var badWebsiteResponse = request("/application/v4/tenant/scoober/info", PUT)
- .data(invalidWebsite)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(badWebsiteResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"'website' needs to be a valid address\"}", 400);
-
- // If any of the address field is set, all fields in address need to be present
- var addressInfo = "{\n" +
- " \"name\": \"Vespa User\",\n" +
- " \"email\": \"user@yahooinc.com\",\n" +
- " \"website\": \"\",\n" +
- " \"contactName\": \"Vespa User\",\n" +
- " \"contactEmail\": \"user@yahooinc.com\",\n" +
- " \"address\": {\n" +
- " \"addressLines\": \"\",\n" +
- " \"postalCodeOrZip\": \"7018\",\n" +
- " \"city\": \"\",\n" +
- " \"stateRegionProvince\": \"\",\n" +
- " \"country\": \"\"\n" +
- " }\n" +
- "}";
- var addressInfoResponse = request("/application/v4/tenant/scoober/info", PUT)
- .data(addressInfo)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(addressInfoResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"All address fields must be set\"}", 400);
-
- // at least one notification activity must be enabled
- var contactsWithoutAudience = "{\"contacts\": [{\"email\": \"contact1@example.com\"}]}";
- var contactsWithoutAudienceResponse = request("/application/v4/tenant/scoober/info", PUT)
- .data(contactsWithoutAudience)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(contactsWithoutAudienceResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"At least one notification activity must be enabled\"}", 400);
-
- // email needs to be present, not blank, and contain an @
- var contactsWithInvalidEmail = "{\"contacts\": [{\"audiences\": [\"tenant\"],\"email\": \"contact1\"}]}";
- var contactsWithInvalidEmailResponse = request("/application/v4/tenant/scoober/info", PUT)
- .data(contactsWithInvalidEmail)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(contactsWithInvalidEmailResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid email address\"}", 400);
-
- // duplicate contact is not allowed
- var contactsWithDuplicateEmail = "{\"contacts\": [{\"audiences\": [\"tenant\"],\"email\": \"contact1@email.com\"}, {\"audiences\": [\"tenant\"],\"email\": \"contact1@email.com\"}]}";
- var contactsWithDuplicateEmailResponse = request("/application/v4/tenant/scoober/info", PUT)
- .data(contactsWithDuplicateEmail)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(contactsWithDuplicateEmailResponse, "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Duplicate contact: email 'contact1@email.com'\"}", 400);
-
- // updating a tenant that already has the fields set works
- var basicInfo = "{\"contactName\": \"Scoober Rentals Inc.\", \"contactEmail\": \"foo@example.com\"}";
- var basicInfoResponse = request("/application/v4/tenant/scoober/info", PUT)
- .data(basicInfo)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(basicInfoResponse, "{\"message\":\"Tenant info updated\"}", 200);
-
- var otherInfo = "{\"billingContact\":{\"name\":\"billingName\"}}";
- var otherInfoResponse = request("/application/v4/tenant/scoober/info", PUT)
- .data(otherInfo)
- .roles(Set.of(Role.administrator(tenantName)));
- tester.assertResponse(otherInfoResponse, "{\"message\":\"Tenant info updated\"}", 200);
- }
-
- @Test
- void trial_tenant_limit_reached() {
- ((InMemoryFlagSource) tester.controller().flagSource()).withIntFlag(PermanentFlags.MAX_TRIAL_TENANTS.id(), 1);
- tester.controller().serviceRegistry().billingController().setPlan(tenantName, PlanId.from("pay-as-you-go"), false, false);
-
- // tests that we can create the one trial tenant the flag says we can have -- and that the tenant created
- // in @Before does not count towards that limit.
- tester.controller().tenants().create(tenantSpec("tenant1"), credentials("administrator"));
-
- // tests that exceeding the limit throws a ForbiddenException
- try {
- tester.controller().tenants().create(tenantSpec("tenant2"), credentials("administrator"));
- fail("Should not be allowed to create tenant that exceed trial limit");
- } catch (RestApiException.Forbidden e) {
- assertEquals("Too many tenants with trial plans, please contact the Vespa support team", e.getMessage());
- }
- }
-
- @Test
- void test_secret_store_configuration() {
- var secretStoreRequest =
- request("/application/v4/tenant/scoober/secret-store/some-name", PUT)
- .data("{" +
- "\"awsId\": \"123\"," +
- "\"role\": \"role-id\"," +
- "\"externalId\": \"321\"" +
- "}")
- .roles(Set.of(Role.developer(tenantName)));
- tester.assertResponse(secretStoreRequest, "{\"secretStores\":[{\"name\":\"some-name\",\"awsId\":\"123\",\"role\":\"role-id\"}]}", 200);
- tester.assertResponse(secretStoreRequest, "{" +
- "\"error-code\":\"BAD_REQUEST\"," +
- "\"message\":\"Secret store TenantSecretStore{name='some-name', awsId='123', role='role-id'} is already configured\"" +
- "}", 400);
-
- secretStoreRequest =
- request("/application/v4/tenant/scoober/secret-store/should-fail", PUT)
- .data("{" +
- "\"awsId\": \" \"," +
- "\"role\": \"role-id\"," +
- "\"externalId\": \"321\"" +
- "}")
- .roles(Set.of(Role.developer(tenantName)));
- tester.assertResponse(secretStoreRequest, "{" +
- "\"error-code\":\"BAD_REQUEST\"," +
- "\"message\":\"Secret store TenantSecretStore{name='should-fail', awsId=' ', role='role-id'} is invalid\"" +
- "}", 400);
- }
-
- @Test
- void validate_secret_store() {
- deployApplication();
- var secretStoreRequest =
- request("/application/v4/tenant/scoober/secret-store/secret-foo/validate?aws-region=us-west-1&parameter-name=foo&application-id=scoober.albums.default&zone=prod.aws-us-east-1c", GET)
- .roles(Set.of(Role.developer(tenantName)));
- tester.assertResponse(secretStoreRequest, "{" +
- "\"error-code\":\"NOT_FOUND\"," +
- "\"message\":\"No secret store 'secret-foo' configured for tenant 'scoober'\"" +
- "}", 404);
-
- tester.controller().tenants().lockOrThrow(tenantName, LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withSecretStore(new TenantSecretStore("secret-foo", "123", "some-role"));
- tester.controller().tenants().store(lockedTenant);
- });
-
- // ConfigServerMock returns message on format deployment.toString() + " - " + tenantSecretStore.toString()
- secretStoreRequest =
- request("/application/v4/tenant/scoober/secret-store/secret-foo/validate?aws-region=us-west-1&parameter-name=foo&application-id=scoober.albums.default&zone=prod.aws-us-east-1c", GET)
- .roles(Set.of(Role.developer(tenantName)));
- tester.assertResponse(secretStoreRequest, "{\"target\":\"scoober.albums in prod.aws-us-east-1c\",\"result\":{\"settings\":{\"name\":\"foo\",\"role\":\"vespa-secretstore-access\",\"awsId\":\"892075328880\",\"externalId\":\"*****\",\"region\":\"us-east-1\"},\"status\":\"ok\"}}", 200);
-
- secretStoreRequest =
- request("/application/v4/tenant/scoober/secret-store/secret-foo/validate?aws-region=us-west-1&parameter-name=foo&application-id=scober.albums.default&zone=prod.aws-us-east-1c", GET)
- .roles(Set.of(Role.developer(tenantName)));
- tester.assertResponse(secretStoreRequest, "{" +
- "\"error-code\":\"BAD_REQUEST\"," +
- "\"message\":\"Invalid application id\"" +
- "}", 400);
- }
-
- @Test
- void delete_secret_store() {
- var deleteRequest =
- request("/application/v4/tenant/scoober/secret-store/secret-foo", DELETE)
- .roles(Set.of(Role.developer(tenantName)));
- tester.assertResponse(deleteRequest, "{" +
- "\"error-code\":\"NOT_FOUND\"," +
- "\"message\":\"Could not delete secret store 'secret-foo': Secret store not found\"" +
- "}", 404);
-
- tester.controller().tenants().lockOrThrow(tenantName, LockedTenant.Cloud.class, lockedTenant -> {
- lockedTenant = lockedTenant.withSecretStore(new TenantSecretStore("secret-foo", "123", "some-role"));
- tester.controller().tenants().store(lockedTenant);
- });
- var tenant = tester.controller().tenants().require(tenantName, CloudTenant.class);
- assertEquals(1, tenant.tenantSecretStores().size());
- tester.assertResponse(deleteRequest, "{\"secretStores\":[]}", 200);
- tenant = tester.controller().tenants().require(tenantName, CloudTenant.class);
- assertEquals(0, tenant.tenantSecretStores().size());
- }
-
- @Test
- void archive_uri_test() {
- ControllerTester wrapped = new ControllerTester(tester);
- wrapped.upgradeSystem(Version.fromString("7.1"));
- new DeploymentTester(wrapped).newDeploymentContext(ApplicationId.from(tenantName, applicationName, InstanceName.defaultName()))
- .submit()
- .deploy();
- tester.controller().tenants().updateCloudAccounts(tenantName, List.of(new CloudAccountInfo(CloudAccount.from("aws:123456789012"), new Version(1, 2, 4))));
-
- tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
- new File("tenant-cloud.json"));
-
- tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/aws", PUT)
- .data("{\"role\":\"arn:aws:iam::123456789012:role/my-role\"}").roles(Role.administrator(tenantName)),
- "{\"message\":\"AWS archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200);
- tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
- (response) -> assertTrue(response.getBodyAsString().contains("\"awsRole\":\"arn:aws:iam::123456789012:role/my-role\"")),
- 200);
- tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/aws", DELETE).roles(Role.administrator(tenantName)),
- "{\"message\":\"AWS archive access role removed for tenant scoober.\"}", 200);
- tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
- (response) -> assertFalse(response.getBodyAsString().contains("\"awsRole\":\"arn:aws:iam::123456789012:role/my-role\"")),
- 200);
-
- tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/gcp", PUT)
- .data("{\"member\":\"user:test@example.com\"}").roles(Role.administrator(tenantName)),
- "{\"message\":\"GCP archive access member set to 'user:test@example.com' for tenant scoober.\"}", 200);
- tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
- (response) -> assertTrue(response.getBodyAsString().contains("\"gcpMember\":\"user:test@example.com\"")),
- 200);
- tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/gcp", DELETE).roles(Role.administrator(tenantName)),
- "{\"message\":\"GCP archive access member removed for tenant scoober.\"}", 200);
- tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
- (response) -> assertFalse(response.getBodyAsString().contains("\"gcpMember\":\"user:test@example.com\"")),
- 200);
-
- tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/aws", PUT)
- .data("{\"role\":\"arn:aws:iam::123456789012:role/my-role\"}").roles(Role.administrator(tenantName)),
- "{\"message\":\"AWS archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200);
- tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
- (response) -> assertTrue(response.getBodyAsString().contains("\"awsRole\":\"arn:aws:iam::123456789012:role/my-role\"")),
- 200);
-
- tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/aws", PUT)
- .data("{\"role\":\"arn:aws:iam::123456789012:role/my-role\"}").roles(Role.administrator(tenantName)),
- "{\"message\":\"AWS archive access role set to 'arn:aws:iam::123456789012:role/my-role' for tenant scoober.\"}", 200);
- tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
- (response) -> assertTrue(response.getBodyAsString().contains("\"awsRole\":\"arn:aws:iam::123456789012:role/my-role\"")),
- 200);
-
- tester.assertResponse(request("/application/v4/tenant/scoober/application/albums/environment/prod/region/aws-us-east-1c/instance/default", GET)
- .roles(Role.reader(tenantName)),
- new File("deployment-cloud.json"));
-
- tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/aws", DELETE).roles(Role.administrator(tenantName)),
- "{\"message\":\"AWS archive access role removed for tenant scoober.\"}", 200);
- tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
- (response) -> assertFalse(response.getBodyAsString().contains("archiveAccessRole")),
- 200);
- }
-
- @Test
- void create_application_on_deploy() {
- var application = ApplicationName.from("unique");
- var applicationPackage = new ApplicationPackageBuilder().trustDefaultCertificate().withoutAthenzIdentity().build();
-
- new ControllerTester(tester).upgradeSystem(new Version("6.1"));
- assertTrue(tester.controller().applications().getApplication(TenantAndApplicationId.from(tenantName, application)).isEmpty());
-
- tester.assertResponse(
- request("/application/v4/tenant/scoober/application/unique/instance/default/deploy/dev-aws-us-east-1c", POST)
- .data(createApplicationDeployData(Optional.of(applicationPackage), Optional.empty(), true))
- .roles(Set.of(Role.developer(tenantName))),
- "{\"message\":\"Deployment started in run 1 of dev-aws-us-east-1c for scoober.unique. This may take about 15 minutes the first time.\",\"run\":1}");
-
- assertTrue(tester.controller().applications().getApplication(TenantAndApplicationId.from(tenantName, application)).isPresent());
- }
-
- @Test
- void create_application_on_submit() {
- var application = ApplicationName.from("unique");
- var applicationPackage = new ApplicationPackageBuilder()
- .trustDefaultCertificate()
- .withoutAthenzIdentity()
- .build();
-
- assertTrue(tester.controller().applications().getApplication(TenantAndApplicationId.from(tenantName, application)).isEmpty());
-
- var data = ApplicationApiTest.createApplicationSubmissionData(applicationPackage, 123);
-
- tester.assertResponse(
- request("/application/v4/tenant/scoober/application/unique/submit", POST)
- .data(data)
- .roles(Set.of(Role.developer(tenantName))),
- "{\"message\":\"application build 1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\",\"build\":1}");
-
- assertTrue(tester.controller().applications().getApplication(TenantAndApplicationId.from(tenantName, application)).isPresent());
- }
-
- @Test
- void dataplane_token_test() {
- tester.assertResponse(request("/application/v4/tenant/scoober/token", GET)
- .roles(Role.developer(tenantName)),
- "{\"tokens\":[]}", 200);
-
- AtomicReference<String> tokenValue = new AtomicReference<>();
- AtomicReference<String> fingerprint = new AtomicReference<>();
- tester.assertResponse(request("/application/v4/tenant/scoober/token/myTokenId", POST).roles(Role.developer(tenantName)),
- (response) -> {
- Cursor root = SlimeUtils.jsonToSlimeOrThrow(response.getBody()).get();
- tokenValue.set(root.field("token").asString());
- fingerprint.set(root.field("fingerprint").asString());
- assertEquals("""
- {
- "id": "myTokenId",
- "token": "%s",
- "fingerprint": "%s",
- "expiration": "2020-10-13T12:26:40Z"
- }
- """.formatted(tokenValue.get(), fingerprint.get()),
- SlimeUtils.toJson(root, false));
- },
- 200);
-
- tester.assertJsonResponse(request("/application/v4/tenant/scoober/token", GET)
- .roles(Role.developer(tenantName)),
- """
- {
- "tokens": [
- {
- "id": "myTokenId",
- "lastUpdatedMillis": 1600000000000,
- "versions": [
- {
- "fingerprint": "%s",
- "created": "2020-09-13T12:26:40Z",
- "author": "user@test",
- "expiration": "2020-10-13T12:26:40Z",
- "state": "unused"
- }
- ]
- }
- ]
- }
- """.formatted(fingerprint.get()),
- 200);
-
- ControllerTester wrapped = new ControllerTester(tester);
- wrapped.upgradeSystem(Version.fromString("7.1"));
- new DeploymentTester(wrapped).newDeploymentContext(ApplicationId.from(tenantName, applicationName, InstanceName.defaultName()))
- .submit()
- .deploy();
- wrapped.serviceRegistry().configServer().activeTokenFingerprints(null)
- .put(HostName.of("host1"), Map.of(TokenId.of("myTokenId"), List.of(FingerPrint.of(fingerprint.get()), FingerPrint.of("ff:01"))));
-
- tester.assertJsonResponse(request("/application/v4/tenant/scoober/token", GET)
- .roles(Role.developer(tenantName)),
- """
- {
- "tokens": [
- {
- "id": "myTokenId",
- "lastUpdatedMillis": 1600000000000,
- "versions": [
- {
- "fingerprint": "%s",
- "created": "2020-09-13T12:26:40Z",
- "author": "user@test",
- "expiration": "2020-10-13T12:26:40Z",
- "state": "active"
- },
- {
- "fingerprint": "ff:01",
- "state": "revoking"
- }
- ]
- }
- ]
- }
- """.formatted(fingerprint.get()),
- 200);
-
- // Rejects invalid tokenIds on create
- tester.assertResponse(request("/application/v4/tenant/scoober/token/foo+bar", POST).roles(Role.developer(tenantName)),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"tokenId must match '[A-Za-z][A-Za-z0-9_-]{0,59}', but got: 'foo bar'\"}",
- 400);
-
- // Rejects invalid tokenIds on delete
- tester.assertResponse(request("/application/v4/tenant/scoober/token/foo+bar?fingerprint=ab:cd", DELETE).roles(Role.developer(tenantName)),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"tokenId must match '[A-Za-z][A-Za-z0-9_-]{0,59}', but got: 'foo bar'\"}",
- 400);
-
- // Rejects invalid fingerprints on delete
- tester.assertResponse(request("/application/v4/tenant/scoober/token/tokenid?fingerprint=ab:cdef", DELETE).roles(Role.developer(tenantName)),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"fingerPrint must match '([a-f0-9]{2}:)+[a-f0-9]{2}', but got: 'ab:cdef'\"}",
- 400);
- }
-
- @Test
- void dataplane_token_endpoint_test() {
- ControllerTester wrapped = new ControllerTester(tester);
- wrapped.upgradeSystem(Version.fromString("7.1"));
- new DeploymentTester(wrapped).newDeploymentContext(ApplicationId.from(tenantName, applicationName, InstanceName.defaultName()))
- .submit()
- .deploy();
-
- tester.assertResponse(request("/application/v4/tenant/scoober/application/albums/environment/prod/region/aws-us-east-1c/instance/default", GET)
- .roles(Role.reader(tenantName)),
- new File("deployment-cloud.json"));
-
- tester.assertResponse(request("/application/v4/tenant/scoober/archive-access/aws", DELETE).roles(Role.administrator(tenantName)),
- "{\"message\":\"AWS archive access role removed for tenant scoober.\"}", 200);
- tester.assertResponse(request("/application/v4/tenant/scoober", GET).roles(Role.reader(tenantName)),
- (response) -> assertFalse(response.getBodyAsString().contains("archiveAccessRole")),
- 200);
- }
-
- private ApplicationPackageBuilder prodBuilder() {
- return new ApplicationPackageBuilder()
- .withoutAthenzIdentity()
- .instances("default")
- .region("aws-us-east-1c");
- }
-
- private void setupTenantAndApplication() {
- var tenantSpec = new CloudTenantSpec(tenantName, "");
- tester.controller().tenants().create(tenantSpec, credentials("developer@scoober"));
-
- var appId = TenantAndApplicationId.from(tenantName, applicationName);
- tester.controller().applications().createApplication(appId, credentials("developer@scoober"));
- }
-
- private static CloudTenantSpec tenantSpec(String name) {
- return new CloudTenantSpec(TenantName.from(name), "");
- }
-
- private static Credentials credentials(String name) {
- return new Auth0Credentials(() -> name, Collections.emptySet());
- }
-
- private void deployApplication() {
- var applicationPackage = new ApplicationPackageBuilder()
- .trustDefaultCertificate()
- .instances("default")
- .endpoint("default", "foo")
- .region("aws-us-east-1c")
- .build();
- new ControllerTester(tester).upgradeSystem(new Version("6.1"));
- tester.controller().jobController().deploy(ApplicationId.from("scoober", "albums", "default"),
- JobType.prod("aws-us-east-1c"),
- Optional.empty(),
- applicationPackage);
- }
-
-
- private MultiPartStreamer createApplicationDeployData(Optional<ApplicationPackage> applicationPackage,
- Optional<ApplicationVersion> applicationVersion, boolean deployDirectly) {
- MultiPartStreamer streamer = new MultiPartStreamer();
- streamer.addJson("deployOptions", deployOptions(deployDirectly, applicationVersion));
- applicationPackage.ifPresent(ap -> streamer.addBytes("applicationZip", ap.zippedContent()));
- return streamer;
- }
-
- private String deployOptions(boolean deployDirectly, Optional<ApplicationVersion> applicationVersion) {
- return "{\"vespaVersion\":null," +
- "\"ignoreValidationErrors\":false," +
- "\"deployDirectly\":" + deployDirectly +
- applicationVersion.map(version ->
- "," +
- "\"buildNumber\":" + version.buildNumber() + "," +
- "\"sourceRevision\":{" +
- "\"repository\":\"" + version.source().get().repository() + "\"," +
- "\"branch\":\"" + version.source().get().branch() + "\"," +
- "\"commit\":\"" + version.source().get().commit() + "\"" +
- "}"
- ).orElse("") +
- "}";
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
deleted file mode 100644
index 66fb17410fd..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java
+++ /dev/null
@@ -1,2048 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import ai.vespa.hosted.api.MultiPartStreamer;
-import ai.vespa.hosted.api.Signatures;
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.AthenzService;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.NodeType;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.security.KeyAlgorithm;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.security.SignatureAlgorithm;
-import com.yahoo.security.X509CertificateBuilder;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.athenz.api.OAuthCredentials;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.api.application.v4.EnvironmentResource;
-import com.yahoo.vespa.hosted.controller.api.application.v4.model.SearchNodeMetrics;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.Property;
-import com.yahoo.vespa.hosted.controller.api.identifiers.PropertyId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.UserId;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.ApplicationVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.AccountId;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.Contact;
-import com.yahoo.vespa.hosted.controller.api.integration.organization.IssueId;
-import com.yahoo.vespa.hosted.controller.application.Deployment;
-import com.yahoo.vespa.hosted.controller.application.DeploymentMetrics;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageTest;
-import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger;
-import com.yahoo.vespa.hosted.controller.integration.ConfigServerMock;
-import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.metric.ApplicationMetrics;
-import com.yahoo.vespa.hosted.controller.notification.NotificationSource;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import com.yahoo.vespa.hosted.controller.routing.RoutingStatus;
-import com.yahoo.vespa.hosted.controller.routing.context.DeploymentRoutingContext;
-import com.yahoo.vespa.hosted.controller.security.AthenzCredentials;
-import com.yahoo.vespa.hosted.controller.security.AthenzTenantSpec;
-import com.yahoo.vespa.hosted.controller.support.access.SupportAccessGrant;
-import com.yahoo.vespa.hosted.controller.tenant.AthenzTenant;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import com.yahoo.yolean.Exceptions;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import javax.security.auth.x500.X500Principal;
-import java.io.File;
-import java.math.BigInteger;
-import java.net.URI;
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.Instant;
-import java.time.format.DateTimeFormatter;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.BiFunction;
-import java.util.function.Supplier;
-
-import static com.yahoo.application.container.handler.Request.Method.DELETE;
-import static com.yahoo.application.container.handler.Request.Method.GET;
-import static com.yahoo.application.container.handler.Request.Method.PATCH;
-import static com.yahoo.application.container.handler.Request.Method.POST;
-import static com.yahoo.application.container.handler.Request.Method.PUT;
-import static com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackageTest.unzip;
-import static java.net.URLEncoder.encode;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.stream.Collectors.joining;
-import static org.junit.jupiter.api.Assertions.assertArrayEquals;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author bratseth
- * @author mpolden
- * @author bjorncs
- * @author jonmv
- */
-public class ApplicationApiTest extends ControllerContainerTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/";
- private static final String pemPublicKey = """
- -----BEGIN PUBLIC KEY-----
- MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9
- z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==
- -----END PUBLIC KEY-----
- """;
- private static final String quotedPemPublicKey = pemPublicKey.replaceAll("\\n", "\\\\n");
- private static final String accessDenied = "{\n \"code\" : 403,\n \"message\" : \"Access denied\"\n}";
-
- private static final ApplicationPackage applicationPackageDefault = new ApplicationPackageBuilder()
- .withoutAthenzIdentity()
- .instances("default")
- .endpoint("default", "foo")
- .region("us-central-1")
- .region("us-east-3")
- .region("us-west-1")
- .blockChange(false, true, "mon-fri", "0-8", "UTC")
- .build();
-
- private static final ApplicationPackage applicationPackageInstance1 = new ApplicationPackageBuilder()
- .withoutAthenzIdentity()
- .instances("instance1")
- .endpoint("default", "foo")
- .region("us-central-1")
- .region("us-east-3")
- .region("us-west-1")
- .blockChange(false, true, "mon-fri", "0-8", "UTC")
- .applicationEndpoint("a0", "foo", "us-central-1", Map.of(InstanceName.from("instance1"), 1))
- .build();
-
- private static final AthenzDomain ATHENZ_TENANT_DOMAIN = new AthenzDomain("domain1");
- private static final AthenzDomain ATHENZ_TENANT_DOMAIN_2 = new AthenzDomain("domain2");
- private static final ScrewdriverId SCREWDRIVER_ID = new ScrewdriverId("12345");
- private static final UserId USER_ID = new UserId("myuser");
- private static final UserId OTHER_USER_ID = new UserId("otheruser");
- private static final UserId HOSTED_VESPA_OPERATOR = new UserId("johnoperator");
- private static final OAuthCredentials OKTA_CREDENTIALS = OAuthCredentials.createForTesting("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.he0ErCNloe4J7Id0Ry2SEDg09lKkZkfsRiGsdX_vgEg", "okta-it");
-
- private static final byte[] testZip = ApplicationPackageTest.zip(Map.of("tests", "content"));
-
- private ContainerTester tester;
- private DeploymentTester deploymentTester;
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, responseFiles);
- deploymentTester = new DeploymentTester(new ControllerTester(tester));
- deploymentTester.controllerTester().computeVersionStatus();
- }
-
- @Test
- void testApplicationApi() {
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID); // (Necessary but not provided in this API)
-
- // GET API root
- tester.assertResponse(request("/application/v4/", GET).userIdentity(USER_ID),
- new File("root.json"));
- // POST (add) a tenant without property ID
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
- .userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("tenant-without-applications.json"));
- // PUT (modify) a tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
- new File("tenant-without-applications.json"));
-
- // Add another Athens domain, so we can try to create more tenants
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN_2, USER_ID); // New domain to test tenant w/property ID
- // Add property info for that property id, as well, in the mock organization.
- registerContact(1234);
-
- // POST (add) a tenant with property ID
- tester.assertResponse(request("/application/v4/tenant/tenant2", POST)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS)
- .data("{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}"),
- new File("tenant-without-applications-with-id.json"));
- // PUT (modify) a tenant with property ID
- tester.assertResponse(request("/application/v4/tenant/tenant2", PUT)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS)
- .data("{\"athensDomain\":\"domain2\", \"property\":\"property2\", \"propertyId\":\"1234\"}"),
- new File("tenant-without-applications-with-id.json"));
- // GET a tenant with property ID and contact information
- updateContactInformation();
- tester.controller().tenants().updateLastLogin(TenantName.from("tenant2"),
- List.of(LastLoginInfo.UserLevel.user, LastLoginInfo.UserLevel.administrator), Instant.ofEpochMilli(1234));
- tester.assertResponse(request("/application/v4/tenant/tenant2", GET).userIdentity(USER_ID),
- new File("tenant2.json"));
-
- // POST (create) an application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", POST)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("instance-reference.json"));
- // GET a tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", GET).userIdentity(USER_ID),
- new File("tenant-with-application.json"));
-
- tester.assertResponse(request("/application/v4/tenant/tenant1", GET)
- .userIdentity(USER_ID)
- .properties(Map.of("activeInstances", "true")),
- new File("tenant-without-applications.json"));
-
- // GET tenant applications
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/", GET).userIdentity(USER_ID),
- new File("application-list.json"));
- // GET tenant application instances for application that does not exist
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/fake-app/instance/", GET).userIdentity(USER_ID),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"Application 'fake-app' does not exist\"}", 404);
-
- // GET tenant applications (instances of "application1" only)
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/", GET).userIdentity(USER_ID),
- new File("application-list.json"));
- // GET at a tenant, with "&recursive=true&production=true", recurses over no instances yet, as they are not in deployment spec.
- tester.assertResponse(request("/application/v4/tenant/tenant1/", GET)
- .userIdentity(USER_ID)
- .properties(Map.of("recursive", "true",
- "production", "true")),
- new File("tenant-with-empty-application.json"));
- // GET at an application, with "&recursive=true&production=true", recurses over no instances yet, as they are not in deployment spec.
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET)
- .userIdentity(USER_ID)
- .properties(Map.of("recursive", "true",
- "production", "true")),
- new File("application-without-instances.json"));
-
- addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR));
-
- ApplicationId id = ApplicationId.from("tenant1", "application1", "instance1");
- var app1 = deploymentTester.newDeploymentContext(id);
-
- // POST (deploy) an application to start a manual deployment in prod is not allowed
- MultiPartStreamer entity = createApplicationDeployData(applicationPackageInstance1);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploy/production-us-east-3/", POST)
- .data(entity)
- .userIdentity(USER_ID),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Direct deployments are only allowed to manually deployed environments.\"}", 400);
-
- // POST (deploy) an application to start a manual deployment in prod is allowed for operators
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploy/production-us-east-3/", POST)
- .data(entity)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"message\":\"Deployment started in run 1 of production-us-east-3 for tenant1.application1.instance1. This may take about 15 minutes the first time.\",\"run\":1}");
- app1.runJob(DeploymentContext.productionUsEast3);
- tester.controller().applications().deactivate(app1.instanceId(), ZoneId.from("prod", "us-east-3"));
-
- // POST (deploy) an application to start a manual deployment to dev
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploy/dev-us-east-1/", POST)
- .data(entity)
- .userIdentity(USER_ID),
- "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for tenant1.application1.instance1. This may take about 15 minutes the first time.\",\"run\":1}");
- app1.runJob(DeploymentContext.devUsEast1);
-
- // POST (deploy) a job to restart a manual deployment to dev
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job/dev-us-east-1", POST)
- .userIdentity(USER_ID),
- "{\"message\":\"Triggered dev-us-east-1 for tenant1.application1.instance1\"}");
- app1.runJob(DeploymentContext.devUsEast1);
-
- // GET dev application package
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job/dev-us-east-1/package", GET)
- .userIdentity(USER_ID),
- (response) -> {
- assertEquals("attachment; filename=\"tenant1.application1.instance1.dev.us-east-1.zip\"", response.getHeaders().getFirst("Content-Disposition"));
- assertArrayEquals(applicationPackageInstance1.zippedContent(), response.getBody());
- },
- 200);
-
- // POST an application package is not generally allowed under user instance
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/otheruser/deploy/dev-us-east-1", POST)
- .userIdentity(OTHER_USER_ID)
- .data(createApplicationDeployData(applicationPackageInstance1)),
- accessDenied,
- 403);
-
- // DELETE a dev deployment is not generally allowed under user instance
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/otheruser/environment/dev/region/us-east-1", DELETE)
- .userIdentity(OTHER_USER_ID),
- accessDenied,
- 403);
-
- // When the user is a tenant admin, user instances are allowed.
- // POST an application package is not allowed under user instance for tenant admins
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/myuser/deploy/dev-us-east-1", POST)
- .userIdentity(USER_ID)
- .data(createApplicationDeployData(applicationPackageInstance1)),
- new File("deployment-job-accepted-2.json"));
-
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/myuser/job/dev-us-east-1/diff/1", GET).userIdentity(HOSTED_VESPA_OPERATOR),
- (response) -> assertTrue(response.getBodyAsString().contains("--- schemas/test.sd\n" +
- "@@ -1,0 +1,1 @@\n" +
- "+ search test { }\n"),
- response.getBodyAsString()),
- 200);
-
- // DELETE a dev deployment is allowed under user instance for tenant admins
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/myuser/environment/dev/region/us-east-1", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Deactivated tenant1.application1.myuser in dev.us-east-1\"}");
-
- // DELETE a user instance
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/myuser", DELETE)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"message\":\"Deleted instance tenant1.application1.myuser\"}");
-
- addScrewdriverUserToDeployRole(SCREWDRIVER_ID,
- ATHENZ_TENANT_DOMAIN,
- id.application());
-
- // POST an application package and a test jar, submitting a new application for production deployment.
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit", POST)
- .screwdriverIdentity(SCREWDRIVER_ID)
- .data(createApplicationSubmissionData(applicationPackageInstance1, 123)),
- "{\"message\":\"application build 1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\",\"build\":1}");
-
- app1.runJob(DeploymentContext.systemTest).runJob(DeploymentContext.stagingTest).runJob(DeploymentContext.productionUsCentral1);
-
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .withoutAthenzIdentity()
- .instances("instance1")
- .endpoint("default", "foo")
- .region("us-west-1")
- .region("us-east-3")
- .allow(ValidationId.globalEndpointChange)
- .build();
-
- // POST (create) another application
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/default", POST)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("instance-reference-2.json"));
-
- ApplicationId id2 = ApplicationId.from("tenant2", "application2", "instance1");
- var app2 = deploymentTester.newDeploymentContext(id2);
- addScrewdriverUserToDeployRole(SCREWDRIVER_ID,
- ATHENZ_TENANT_DOMAIN_2,
- id2.application());
-
- // POST an application package and a test jar, submitting a new application for production deployment.
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/submit", POST)
- .screwdriverIdentity(SCREWDRIVER_ID)
- .data(createApplicationSubmissionData(applicationPackage, 1000)),
- "{\"message\":\"application build 1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\",\"build\":1}");
-
- deploymentTester.triggerJobs();
-
- // POST a triggering to force a production job to start without successful tests
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/instance1/job/production-us-west-1", POST)
- .data("{ \"skipTests\": true, \"skipRevision\": true, \"skipUpgrade\": true }")
- .userIdentity(USER_ID),
- "{\"message\":\"Triggered production-us-west-1 for tenant2.application2.instance1, without revision and platform upgrade\"}");
- app2.runJob(DeploymentContext.productionUsWest1);
-
- // POST a re-triggering to force a production job to start with previous parameters
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/instance1/job/production-us-west-1", POST)
- .data("{\"reTrigger\":true}")
- .userIdentity(USER_ID),
- "{\"message\":\"Triggered production-us-west-1 for tenant2.application2.instance1\"}");
-
- // DELETE manually deployed prod deployment again
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/instance1/environment/prod/region/us-west-1", DELETE)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"message\":\"Deactivated tenant2.application2.instance1 in prod.us-west-1\"}");
-
- // GET application having both change and outstanding change
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET)
- .screwdriverIdentity(SCREWDRIVER_ID),
- new File("application2.json"));
-
- // PATCH in a major version override
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", PATCH)
- .userIdentity(USER_ID)
- .data("{\"majorVersion\":7}"),
- "{\"message\":\"Set major version to 7\"}");
-
- // POST a pem deploy key
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/key", POST)
- .userIdentity(USER_ID)
- .data("{\"key\":\"" + pemPublicKey + "\"}"),
- "{\"keys\":[\"-----BEGIN PUBLIC KEY-----\\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\\n-----END PUBLIC KEY-----\\n\"]}");
-
- // PATCH in a pem deploy key at deprecated path
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/default", PATCH)
- .userIdentity(USER_ID)
- .data("{\"pemDeployKey\":\"" + pemPublicKey + "\"}"),
- "{\"message\":\"Added deploy key " + quotedPemPublicKey + "\"}");
-
- // GET an application with a major version override
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET)
- .userIdentity(USER_ID),
- new File("application2-with-patches.json"));
-
- // PATCH in removal of the application major version override removal
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", PATCH)
- .userIdentity(USER_ID)
- .data("{\"majorVersion\":null}"),
- "{\"message\":\"Set major version to empty\"}");
-
- // GET compile version for an application
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/compile-version", GET)
- .userIdentity(USER_ID),
- "{\"compileVersion\":\"6.1.0\"}");
-
- // DELETE the pem deploy key
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/key", DELETE)
- .userIdentity(USER_ID)
- .data("{\"key\":\"" + pemPublicKey + "\"}"),
- "{\"keys\":[]}");
-
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", GET)
- .userIdentity(USER_ID),
- new File("application2.json"));
-
- // DELETE instance 1 of 2
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/instance/default", DELETE)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"message\":\"Deleted instance tenant2.application2.default\"}");
-
- // DELETE application with only one instance left
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2", DELETE)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"message\":\"Deleted application tenant2.application2\"}");
-
- // Set version 6.1 to broken to change compile version for.
- deploymentTester.upgrader().overrideConfidence(Version.fromString("6.1"), VespaVersion.Confidence.broken);
- deploymentTester.controllerTester().computeVersionStatus();
- setDeploymentMaintainedInfo();
-
- // GET tenant application deployments
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", GET)
- .userIdentity(USER_ID),
- new File("instance.json"));
- // GET an application deployment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1", GET)
- .userIdentity(USER_ID),
- new File("deployment.json"));
-
- addIssues(deploymentTester, TenantAndApplicationId.from("tenant1", "application1"));
- // GET at root, with "&recursive=deployment", returns info about all tenants, their applications and their deployments
- tester.assertResponse(request("/application/v4/", GET)
- .userIdentity(USER_ID)
- .recursive("deployment"),
- new File("recursive-root.json"));
- // GET at root, with "&recursive=tenant", returns info about all tenants, with limited info about their applications.
- tester.assertResponse(request("/application/v4/", GET)
- .userIdentity(USER_ID)
- .recursive("tenant"),
- new File("recursive-until-tenant-root.json"));
- // GET at a tenant, with "&recursive=true", returns full info about their applications and their deployments
- tester.assertResponse(request("/application/v4/tenant/tenant1/", GET)
- .userIdentity(USER_ID)
- .recursive("true"),
- new File("tenant1-recursive.json"));
- // GET at an application, with "&recursive=true", returns full info about its deployments
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", GET)
- .userIdentity(USER_ID)
- .recursive("true"),
- new File("instance1-recursive.json"));
-
- // GET nodes
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/nodes", GET)
- .userIdentity(USER_ID),
- new File("application-nodes.json"));
-
- // GET clusters
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/clusters", GET)
- .userIdentity(USER_ID),
- new File("application-clusters.json"));
-
- // GET logs
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/environment/dev/region/us-east-1/instance/default/logs?from=1233&to=3214", GET)
- .userIdentity(USER_ID),
- "INFO - All good");
-
- // GET controller logs
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/environment/prod/region/controller/instance/default/logs?from=1233&to=3214", GET)
- .userIdentity(USER_ID),
- "INFO - All good");
-
- // Get content/../foo
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/instance/default/environment/dev/region/us-east-1/content/%2E%2E%2Ffoo", GET).userIdentity(USER_ID),
- accessDenied, 403);
- // Get content - root
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/instance/default/environment/dev/region/us-east-1/content/", GET).userIdentity(USER_ID),
- "{\"path\":\"/\"}");
- // Get content - ignore query params
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/instance/default/environment/dev/region/us-east-1/content/bar/file.json?query=param", GET).userIdentity(USER_ID),
- "{\"path\":\"/bar/file.json\"}");
-
- // Drop documents
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/drop-documents", POST)
- .userIdentity(USER_ID),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Drop documents status is only available for manually deployed environments\"}", 400);
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/instance/default/environment/dev/region/us-east-1/drop-documents", POST)
- .userIdentity(USER_ID),
- "{\"message\":\"Triggered drop documents for tenant2.application1.default in dev.us-east-1\"}");
-
- ZoneId zone = ZoneId.from("dev", "us-east-1");
- ApplicationId application = ApplicationId.from("tenant2", "application1", "default");
- BiFunction<Integer, String, Node> nodeBuilder = (index, dropDocumentsReport) -> Node.builder().hostname("node" + index + ".dev.us-east-1.test")
- .state(Node.State.active).type(NodeType.tenant).owner(application).clusterId("c1").clusterType(Node.ClusterType.content)
- .reports(dropDocumentsReport == null ? Map.of() : Map.of("dropDocuments", dropDocumentsReport)).build();
- NodeRepositoryMock nodeRepository = deploymentTester.controllerTester().serviceRegistry().configServer().nodeRepository();
-
- // 2 nodes, neither ever dropped any documents
- nodeRepository.putNodes(zone, List.of(nodeBuilder.apply(1, null), nodeBuilder.apply(2, null)));
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/instance/default/environment/dev/region/us-east-1/drop-documents", GET).userIdentity(USER_ID),
- "{}");
-
- // 1 node previously dropped documents, 1 node without any report
- nodeRepository.putNodes(zone, List.of(nodeBuilder.apply(1, "{\"droppedAt\":1,\"readiedAt\":2,\"startedAt\":3}"), nodeBuilder.apply(2, null)));
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/instance/default/environment/dev/region/us-east-1/drop-documents", GET).userIdentity(USER_ID),
- "{\"lastDropped\":2}");
-
- nodeRepository.putNodes(zone, List.of(nodeBuilder.apply(1, "{}"), nodeBuilder.apply(2, null)));
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/instance/default/environment/dev/region/us-east-1/drop-documents", GET).userIdentity(USER_ID),
- "{\"error-code\":\"CONFLICT\",\"message\":\"Last dropping of documents may have failed to clear all documents due to concurrent topology changes, consider retrying\"}", 409);
-
- nodeRepository.putNodes(zone, List.of(nodeBuilder.apply(1, "{}"), nodeBuilder.apply(2, "{\"droppedAt\":1}")));
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/instance/default/environment/dev/region/us-east-1/drop-documents", GET).userIdentity(USER_ID),
- "{\"progress\":{\"total\":2,\"dropped\":1,\"started\":0}}");
-
- nodeRepository.putNodes(zone, List.of(nodeBuilder.apply(1, "{\"startedAt\":3}"), nodeBuilder.apply(2, "{\"readiedAt\":1}")));
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application1/instance/default/environment/dev/region/us-east-1/drop-documents", GET).userIdentity(USER_ID),
- "{\"progress\":{\"total\":2,\"dropped\":2,\"started\":1}}");
-
- updateMetrics();
-
- // GET metrics
- tester.assertJsonResponse(request("/application/v4/tenant/tenant2/application/application1/environment/dev/region/us-east-1/instance/default/metrics", GET)
- .userIdentity(USER_ID),
- new File("proton-metrics.json"));
-
- // POST a roll-out of the latest application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/application", POST)
- .userIdentity(USER_ID),
- "{\"message\":\"Triggered revision change to build 1 for tenant1.application1.instance1\"}");
-
- // POST a roll-out of a given revision
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/application", POST)
- .data("{ \"build\": 1 }")
- .userIdentity(USER_ID),
- "{\"message\":\"Triggered revision change to build 1 for tenant1.application1.instance1\"}");
-
- // DELETE (cancel) ongoing change
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", DELETE)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"message\":\"Changed deployment from 'revision change to build 1' to 'no change' for tenant1.application1.instance1\"}");
-
- // DELETE (cancel) again is a no-op
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", DELETE)
- .userIdentity(USER_ID)
- .data("{\"cancel\":\"all\"}"),
- "{\"message\":\"No deployment in progress for tenant1.application1.instance1 at this time\"}");
-
- // POST pinning to a given version to an application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/platform-pin", POST)
- .userIdentity(USER_ID)
- .data("6.1.0"),
- "{\"message\":\"Triggered pin to 6.1 for tenant1.application1.instance1\"}");
- assertTrue(tester.controller().auditLogger().readLog().entries().stream()
- .anyMatch(entry -> entry.resource().equals("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/platform-pin?")),
- "Action is logged to audit log");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET)
- .userIdentity(USER_ID), "{\"platform\":\"6.1\",\"pinned\":true,\"platform-pinned\":true,\"application-pinned\":false}");
-
- // DELETE only the pin to a given version
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/platform-pin", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Changed deployment from 'pin to 6.1' to 'upgrade to 6.1' for tenant1.application1.instance1\"}");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET)
- .userIdentity(USER_ID), "{\"platform\":\"6.1\",\"pinned\":false,\"platform-pinned\":false,\"application-pinned\":false}");
-
- // POST pinning again
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/pin", POST)
- .userIdentity(USER_ID)
- .data("6.1"),
- "{\"message\":\"Triggered pin to 6.1 for tenant1.application1.instance1\"}");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET)
- .userIdentity(USER_ID), "{\"platform\":\"6.1\",\"pinned\":true,\"platform-pinned\":true,\"application-pinned\":false}");
-
- // DELETE only the version, but leave the pin
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/platform", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Changed deployment from 'pin to 6.1' to 'pin to current platform' for tenant1.application1.instance1\"}");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET)
- .userIdentity(USER_ID), "{\"pinned\":true,\"platform-pinned\":true,\"application-pinned\":false}");
-
- // DELETE also the pin to a given version
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/pin", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Changed deployment from 'pin to current platform' to 'no change' for tenant1.application1.instance1\"}");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET)
- .userIdentity(USER_ID), "{}");
-
- // POST pinning to a given revision to an application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/application-pin", POST)
- .userIdentity(USER_ID)
- .data(""),
- "{\"message\":\"Triggered pin to build 1 for tenant1.application1.instance1\"}");
- assertTrue(tester.controller().auditLogger().readLog().entries().stream()
- .anyMatch(entry -> entry.resource().equals("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/application-pin?")),
- "Action is logged to audit log");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET)
- .userIdentity(USER_ID), "{\"application\":\"build 1\",\"pinned\":false,\"platform-pinned\":false,\"application-pinned\":true}");
-
- // DELETE only the pin to a given revision
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/application-pin", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Changed deployment from 'pin to build 1' to 'revision change to build 1' for tenant1.application1.instance1\"}");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET)
- .userIdentity(USER_ID), "{\"application\":\"build 1\",\"pinned\":false,\"platform-pinned\":false,\"application-pinned\":false}");
-
- // DELETE deploying to a given revision
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying/application", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Changed deployment from 'revision change to build 1' to 'no change' for tenant1.application1.instance1\"}");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploying", GET)
- .userIdentity(USER_ID), "{}");
-
-
- // POST a pause to a production job
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-west-1/pause", POST)
- .userIdentity(USER_ID),
- "{\"message\":\"production-us-west-1 for tenant1.application1.instance1 paused for " + DeploymentTrigger.maxPause + "\"}");
-
- // DELETE a pause of a production job
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-west-1/pause", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"production-us-west-1 for tenant1.application1.instance1 resumed\"}");
-
- // POST a triggering to the same production job
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-west-1", POST)
- .userIdentity(USER_ID),
- "{\"message\":\"Triggered production-us-west-1 for tenant1.application1.instance1\"}");
-
- // POST a 'reindex application' command
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/reindex", POST)
- .properties(Map.of("indexedOnly", "true",
- "speed", "10"))
- .userIdentity(USER_ID),
- "{\"message\":\"Requested reindexing of tenant1.application1.instance1 in prod.us-central-1, for indexed types, with speed 10.0\"}");
-
- // POST a 'reindex application' command with cluster filter
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/reindex", POST)
- .properties(Map.of("clusterId", "boo,moo"))
- .userIdentity(USER_ID),
- "{\"message\":\"Requested reindexing of tenant1.application1.instance1 in prod.us-central-1, on clusters boo, moo\"}");
-
- // POST a 'reindex application' command with cluster and document type filters
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/reindex", POST)
- .properties(Map.of("clusterId", "boo,moo",
- "documentType", "foo,boo"))
- .userIdentity(USER_ID),
- "{\"message\":\"Requested reindexing of tenant1.application1.instance1 in prod.us-central-1, on clusters boo, moo, for types foo, boo\"}");
-
- // POST to enable reindexing
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/reindexing", POST)
- .userIdentity(USER_ID),
- "{\"message\":\"Enabled reindexing of tenant1.application1.instance1 in prod.us-central-1\"}");
-
- // DELETE to disable reindexing
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/reindexing", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Disabled reindexing of tenant1.application1.instance1 in prod.us-central-1\"}");
-
- // GET to get reindexing status
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/reindexing", GET)
- .userIdentity(USER_ID),
- "{\"enabled\":true,\"clusters\":[{\"name\":\"cluster\",\"pending\":[{\"type\":\"type\",\"requiredGeneration\":100}],\"ready\":[{\"type\":\"type\",\"readyAtMillis\":345,\"startedAtMillis\":456,\"endedAtMillis\":567,\"state\":\"failed\",\"message\":\"(#`д´)ノ\",\"progress\":0.1,\"speed\":1.0,\"cause\":\"test reindexing\"}]}]}");
-
- // POST to request a service dump
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/node/host-tenant1.application1.instance1-prod.us-central-1/service-dump", POST)
- .userIdentity(HOSTED_VESPA_OPERATOR)
- .data("{\"configId\":\"default/container.1\",\"artifacts\":[\"jvm-dump\"],\"dumpOptions\":{\"duration\":30}}"),
- "{\"message\":\"Request created\"}");
-
- // GET to get status of service dump
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/node/host-tenant1.application1.instance1-prod.us-central-1/service-dump", GET)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"createdMillis\":" + tester.controller().clock().millis() + ",\"configId\":\"default/container.1\"" +
- ",\"artifacts\":[\"jvm-dump\"],\"dumpOptions\":{\"duration\":30}}");
-
- // POST a 'restart application' command
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/restart", POST)
- .userIdentity(USER_ID),
- "{\"message\":\"Requested restart of tenant1.application1.instance1 in prod.us-central-1\"}");
-
- // POST a 'restart application' command
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/restart", POST)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"message\":\"Requested restart of tenant1.application1.instance1 in prod.us-central-1\"}");
-
- addUserToHostedOperatorRole(HostedAthenzIdentities.from(SCREWDRIVER_ID));
-
- // POST a 'restart application' in staging environment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/staging/region/us-east-3/instance/instance1/restart", POST)
- .screwdriverIdentity(SCREWDRIVER_ID),
- "{\"message\":\"Requested restart of tenant1.application1.instance1 in staging.us-east-3\"}");
-
- // POST a 'restart application' in test environment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/test/region/us-east-1/instance/instance1/restart", POST)
- .screwdriverIdentity(SCREWDRIVER_ID),
- "{\"message\":\"Requested restart of tenant1.application1.instance1 in test.us-east-1\"}");
-
- // POST a 'restart application' in dev environment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-east-1/instance/instance1/restart", POST)
- .userIdentity(USER_ID),
- "{\"message\":\"Requested restart of tenant1.application1.instance1 in dev.us-east-1\"}");
-
- // POST a 'restart application' command with a host filter (other filters not supported yet)
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/restart", POST)
- .properties(Map.of("hostname", "node-1-tenant-host-prod.us-central-1"))
- .screwdriverIdentity(SCREWDRIVER_ID),
- "{\"message\":\"Requested restart of tenant1.application1.instance1 in prod.us-central-1\"}", 200);
-
- // POST a 'suspend application' in dev environment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/dev/region/us-east-1/suspend", POST)
- .userIdentity(USER_ID),
- "{\"message\":\"Suspended orchestration of tenant1.application1.instance1 in dev.us-east-1\"}");
-
- // POST a 'resume application' in dev environment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/dev/region/us-east-1/suspend", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Resumed orchestration of tenant1.application1.instance1 in dev.us-east-1\"}");
-
- // POST a 'suspend application' in prod environment fails
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-east-3/suspend", POST)
- .userIdentity(USER_ID),
- accessDenied, 403);
-
- // GET suspended
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1/suspended", GET)
- .userIdentity(USER_ID),
- new File("suspended.json"));
-
- // GET private service info
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/private-services", GET)
- .userIdentity(USER_ID),
- """
- {"privateServices":[{"cluster":"default","serviceId":"service","type":"unknown","allowedUrns":[{"type":"aws-private-link","urn":"arne"}],"endpoints":[{"endpointId":"endpoint-1","state":"open","detail":"available"}]}]}""");
-
- // GET service/state/v1
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/service/storagenode/host.com/state/v1/?foo=bar", GET)
- .userIdentity(USER_ID),
- new File("service"));
-
- // GET orchestrator
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/orchestrator", GET)
- .userIdentity(USER_ID),
- "{\"json\":\"thank you very much\"}");
-
- // GET application package which has been deployed to production
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package", GET)
- .properties(Map.of("build", "latestDeployed"))
- .userIdentity(HOSTED_VESPA_OPERATOR),
- (response) -> {
- assertEquals("attachment; filename=\"tenant1.application1-build1.zip\"", response.getHeaders().getFirst("Content-Disposition"));
- assertArrayEquals(applicationPackageInstance1.zippedContent(), response.getBody());
- },
- 200);
-
- // GET searches deployments by endpoints
- tester.assertResponse(request("/application/v4/search/deployment", GET).userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Missing 'endpoint' query parameter\"}", 400);
- tester.assertResponse(request("/application/v4/search/deployment", GET).properties(Map.of("endpoint", "https://instance1.application1.tenant1.global.vespa.oath.cloud:4443"))
- .userIdentity(HOSTED_VESPA_OPERATOR),
- new File("search-deployments-multi.json"), 200);
- tester.assertResponse(request("/application/v4/search/deployment", GET).properties(Map.of("endpoint", "instance1.application1.tenant1.global.vespa.oath.cloud"))
- .userIdentity(HOSTED_VESPA_OPERATOR),
- new File("search-deployments-multi.json"), 200);
- tester.assertResponse(request("/application/v4/search/deployment", GET).properties(Map.of("endpoint", "instance1.application1.tenant1.us-central-1.vespa.oath.cloud"))
- .userIdentity(HOSTED_VESPA_OPERATOR),
- new File("search-deployments-single.json"), 200);
- tester.assertResponse(request("/application/v4/search/deployment", GET).properties(Map.of("endpoint", "non-existent"))
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"deployments\":[]}", 200);
-
- // DELETE application with active deployments fails
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", DELETE)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("delete-with-active-deployments.json"), 400);
-
- // DELETE (deactivate) a deployment - dev
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-east-1/instance/instance1", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Deactivated tenant1.application1.instance1 in dev.us-east-1\"}");
-
- // DELETE (deactivate) a deployment - prod
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1", DELETE)
- .screwdriverIdentity(SCREWDRIVER_ID),
- "{\"message\":\"Deactivated tenant1.application1.instance1 in prod.us-central-1\"}");
-
-
- // DELETE (deactivate) a deployment is idempotent
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-central-1/instance/instance1", DELETE)
- .screwdriverIdentity(SCREWDRIVER_ID),
- "{\"message\":\"Deactivated tenant1.application1.instance1 in prod.us-central-1\"}");
-
- // Setup for test config tests
- tester.controller().jobController().deploy(ApplicationId.from("tenant1", "application1", "default"),
- DeploymentContext.productionUsCentral1,
- Optional.empty(),
- applicationPackageDefault);
- tester.controller().jobController().deploy(ApplicationId.from("tenant1", "application1", "my-user"),
- DeploymentContext.devUsEast1,
- Optional.empty(),
- applicationPackageDefault);
-
- // GET test-config for local tests against a dev deployment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/my-user/job/dev-us-east-1/test-config", GET)
- .userIdentity(USER_ID),
- new File("test-config-dev.json"));
- // GET test-config for local tests against a prod deployment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/my-user/job/production-us-central-1/test-config", GET)
- .userIdentity(USER_ID),
- new File("test-config.json"));
- tester.controller().applications().deactivate(ApplicationId.from("tenant1", "application1", "default"),
- ZoneId.from("prod", "us-central-1"));
- tester.controller().applications().deactivate(ApplicationId.from("tenant1", "application1", "my-user"),
- ZoneId.from("dev", "us-east-1"));
- // teardown for test config tests
-
- // Second attempt has a service under a different domain than the tenant of the application, and fails.
- ApplicationPackage packageWithServiceForWrongDomain = new ApplicationPackageBuilder()
- .instances("instance1")
- .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from(ATHENZ_TENANT_DOMAIN_2.getName()), AthenzService.from("service"))
- .region("us-west-1")
- .build();
- allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN_2, "service"));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST)
- .screwdriverIdentity(SCREWDRIVER_ID)
- .data(createApplicationSubmissionData(packageWithServiceForWrongDomain, 123)),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Athenz domain in deployment.xml: [domain2] must match tenant domain: [domain1]\"}", 400);
-
- // Third attempt has a service under the domain of the tenant, and also succeeds.
- ApplicationPackage packageWithService = new ApplicationPackageBuilder()
- .instances("instance1")
- .endpoint("default", "foo")
- .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from(ATHENZ_TENANT_DOMAIN.getName()), AthenzService.from("service"))
- .region("us-central-1")
- .parallel("us-west-1", "us-east-3")
- .build();
- allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST)
- .screwdriverIdentity(SCREWDRIVER_ID)
- .data(createApplicationSubmissionData(packageWithService, 123)),
- "{\"message\":\"application build 2, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\",\"build\":2}");
-
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/diff/2", GET).userIdentity(HOSTED_VESPA_OPERATOR),
- (response) -> assertTrue(response.getBodyAsString().contains("+ <deployment version='1.0' athenz-domain='domain1' athenz-service='service'>\n" +
- "- <deployment version='1.0' >\n"),
- response.getBodyAsString()),
- 200);
-
- // GET last submitted application package
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package", GET).userIdentity(HOSTED_VESPA_OPERATOR),
- (response) -> {
- assertEquals("attachment; filename=\"tenant1.application1-build2.zip\"", response.getHeaders().getFirst("Content-Disposition"));
- assertArrayEquals(packageWithService.zippedContent(), response.getBody());
- },
- 200);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package", GET).userIdentity(HOSTED_VESPA_OPERATOR).properties(Map.of("tests", "true")),
- (response) -> {
- assertEquals("attachment; filename=\"tenant1.application1-tests2.zip\"", response.getHeaders().getFirst("Content-Disposition"));
- assertEquals(Map.of("tests", "content", "deployment.xml", packageWithService.deploymentSpec().xmlForm()),
- unzip(response.getBody()));
- },
- 200);
-
- // GET application package for specific build
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package", GET)
- .properties(Map.of("build", "2"))
- .userIdentity(HOSTED_VESPA_OPERATOR),
- (response) -> {
- assertEquals("attachment; filename=\"tenant1.application1-build2.zip\"", response.getHeaders().getFirst("Content-Disposition"));
- assertArrayEquals(packageWithService.zippedContent(), response.getBody());
- },
- 200);
-
- // Fourth attempt has a wrong content hash in a header, and fails.
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST)
- .screwdriverIdentity(SCREWDRIVER_ID)
- .header("X-Content-Hash", "not/the/right/hash")
- .data(createApplicationSubmissionData(packageWithService, 123)),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Value of X-Content-Hash header does not match computed content hash\"}", 400);
-
- // Fifth attempt has the right content hash in a header, and succeeds.
- MultiPartStreamer streamer = createApplicationSubmissionData(packageWithService, 123);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST)
- .screwdriverIdentity(SCREWDRIVER_ID)
- .header("X-Content-Hash", Base64.getEncoder().encodeToString(Signatures.sha256Digest(streamer::data)))
- .data(streamer),
- """
- {"message":"application build 3, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z; only applying deployment spec changes, as this build is otherwise equal to the previous","build":3}""");
-
- // Sixth attempt has a multi-instance deployment spec, and is accepted.
- ApplicationPackage multiInstanceSpec = new ApplicationPackageBuilder()
- .withoutAthenzIdentity()
- .instances("instance1,instance2")
- .region("us-central-1")
- .parallel("us-west-1", "us-east-3")
- .endpoint("default", "foo", "us-central-1", "us-west-1", "us-east-3")
- .build();
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/submit", POST)
- .screwdriverIdentity(SCREWDRIVER_ID)
- .data(createApplicationSubmissionData(multiInstanceSpec, 123)),
- "{\"message\":\"application build 4, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\",\"build\":4}");
-
-
- // DELETE submitted build, to mark it as non-deployable
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit/2", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Marked build '2' as non-deployable\"}");
-
- // GET deployment job overview, after triggering system and staging test jobs.
- assertEquals(2, tester.controller().applications().deploymentTrigger().triggerReadyJobs().triggered());
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job", GET)
- .userIdentity(USER_ID),
- new File("jobs.json"));
-
- // GET deployment job overview for whole application.
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deployment", GET)
- .userIdentity(USER_ID),
- new File("deployment-overview.json"));
-
- // GET system test job overview.
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test", GET)
- .userIdentity(USER_ID),
- new File("system-test-job.json"));
-
- // GET system test run 1 details.
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test/run/1", GET)
- .userIdentity(USER_ID),
- new File("system-test-details.json"));
-
- // DELETE a running job to have it aborted.
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/job/staging-test", DELETE)
- .userIdentity(USER_ID),
- "{\"message\":\"Aborting run 2 of staging-test for tenant1.application1.instance1\"}");
-
- // GET compile version for specific major
- deploymentTester.controllerTester().upgradeSystem(Version.fromString("7.0"));
- deploymentTester.controllerTester().flagSource().withListFlag(PermanentFlags.INCOMPATIBLE_VERSIONS.id(), List.of("*"), String.class);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/compile-version", GET)
- .userIdentity(USER_ID).properties(Map.of("allowMajor", "7")),
- "{\"compileVersion\":\"7.0.0\"}");
-
- // OPTIONS return 200 OK
- tester.assertResponse(request("/application/v4/", Request.Method.OPTIONS)
- .userIdentity(USER_ID),
- "");
-
- addNotifications(TenantName.from("tenant1"));
- addNotifications(TenantName.from("tenant2"));
- tester.assertResponse(request("/application/v4/notifications", GET)
- .properties(Map.of("type", "applicationPackage", "excludeMessages", "true")).userIdentity(HOSTED_VESPA_OPERATOR),
- new File("notifications-applicationPackage.json"));
- tester.assertResponse(request("/application/v4/tenant/tenant1/notifications", GET).userIdentity(USER_ID),
- new File("notifications-tenant1.json"));
- tester.assertResponse(request("/application/v4/tenant/tenant1/notifications", GET)
- .properties(Map.of("application", "app2")).userIdentity(USER_ID),
- new File("notifications-tenant1-app2.json"));
-
- // DELETE the application which no longer has any deployments
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"message\":\"Deleted application tenant1.application1\"}");
-
- // DELETE an empty tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE).userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"message\":\"Deleted tenant tenant1\"}");
-
- // The tenant is not found
- tester.assertResponse(request("/application/v4/tenant/tenant1", GET).userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'tenant1' does not exist\"}", 404);
-
- // ... unless we specify to show deleted tenants
- tester.assertResponse(request("/application/v4/tenant/tenant1", GET).properties(Map.of("includeDeleted", "true"))
- .userIdentity(HOSTED_VESPA_OPERATOR),
- new File("tenant1-deleted.json"));
-
- // Tenant cannot be recreated
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST).userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS),
- """
- {"error-code":"BAD_REQUEST","message":"Tenant 'tenant1' cannot be created, try a different name"}""", 400);
-
-
- // Forget a deleted tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE).properties(Map.of("forget", "true"))
- .data("{\"athensDomain\":\"domain1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"message\":\"Deleted tenant tenant1\"}");
- tester.assertResponse(request("/application/v4/tenant/tenant1", GET).properties(Map.of("includeDeleted", "true"))
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'tenant1' does not exist\"}", 404);
- }
-
- private void addIssues(DeploymentTester tester, TenantAndApplicationId id) {
- tester.applications().lockApplicationOrThrow(id, application ->
- tester.controller().applications().store(application.withDeploymentIssueId(IssueId.from("123"))
- .withOwnershipIssueId(IssueId.from("321"))
- .withOwner(new AccountId("owner-account-id"))));
- }
-
- @Test
- void testRotationOverride() {
- // Setup
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
- var westZone = ZoneId.from("prod", "us-west-1");
- var eastZone = ZoneId.from("prod", "us-east-3");
- var applicationPackage = new ApplicationPackageBuilder()
- .instances("instance1")
- .endpoint("default", "foo")
- .region(westZone.region())
- .region(eastZone.region())
- .build();
-
- // Create tenant and deploy
- var app = deploymentTester.newDeploymentContext(createTenantAndApplication());
- app.submit(applicationPackage).deploy();
-
- // Invalid application fails
- tester.assertResponse(request("/application/v4/tenant/tenant2/application/application2/environment/prod/region/us-west-1/instance/default/global-rotation", GET)
- .userIdentity(USER_ID),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"tenant2.application2 not found\"}",
- 400);
-
- // Invalid deployment fails
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/global-rotation", GET)
- .userIdentity(USER_ID),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"application instance 'tenant1.application1.instance1' has no deployment in prod.us-central-1\"}",
- 404);
-
- // Change status of non-existing deployment fails
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/global-rotation/override", PUT)
- .userIdentity(USER_ID)
- .data("{\"reason\":\"unit-test\"}"),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"application instance 'tenant1.application1.instance1' has no deployment in prod.us-central-1\"}",
- 404);
-
- // GET global rotation status
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation", GET)
- .userIdentity(USER_ID),
- new File("global-rotation.json"));
-
- // GET global rotation override status
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation/override", GET)
- .userIdentity(USER_ID),
- new File("global-rotation-get.json"));
-
- // SET global rotation override status
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation/override", PUT)
- .userIdentity(USER_ID)
- .data("{\"reason\":\"unit-test\"}"),
- new File("global-rotation-put.json"));
-
- // Status of routing policy is changed
- assertGlobalRouting(app.deploymentIdIn(westZone), RoutingStatus.Value.out, RoutingStatus.Agent.tenant);
-
- // DELETE global rotation override status
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation/override", DELETE)
- .userIdentity(USER_ID)
- .data("{\"reason\":\"unit-test\"}"),
- new File("global-rotation-delete.json"));
- assertGlobalRouting(app.deploymentIdIn(westZone), RoutingStatus.Value.in, RoutingStatus.Agent.tenant);
-
- // SET global rotation override status by operator
- addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation/override", PUT)
- .userIdentity(HOSTED_VESPA_OPERATOR)
- .data("{\"reason\":\"unit-test\"}"),
- new File("global-rotation-put.json"));
- assertGlobalRouting(app.deploymentIdIn(westZone), RoutingStatus.Value.out, RoutingStatus.Agent.operator);
- }
-
- @Test
- void multiple_endpoints() {
- // Setup
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .instances("instance1")
- .region("us-west-1")
- .region("us-east-3")
- .region("eu-west-1")
- .endpoint("eu", "default", "eu-west-1")
- .endpoint("default", "default", "us-west-1", "us-east-3")
- .build();
-
- // Create tenant and deploy
- var app = deploymentTester.newDeploymentContext("tenant1", "application1", "instance1");
- app.submit(applicationPackage).deploy();
-
- // GET global rotation status without specifying endpointId fails
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation", GET)
- .userIdentity(USER_ID),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"application instance 'tenant1.application1.instance1' has multiple rotations. Query parameter 'endpointId' must be given\"}",
- 400);
-
- // GET global rotation status for us-west-1 in default endpoint
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation", GET)
- .properties(Map.of("endpointId", "default"))
- .userIdentity(USER_ID),
- "{\"bcpStatus\":{\"rotationStatus\":\"UNKNOWN\"}}",
- 200);
-
- // GET global rotation status for us-west-1 in eu endpoint
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/global-rotation", GET)
- .properties(Map.of("endpointId", "eu"))
- .userIdentity(USER_ID),
- "{\"bcpStatus\":{\"rotationStatus\":\"UNKNOWN\"}}",
- 200);
-
- // GET global rotation status for eu-west-1 in eu endpoint
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/eu-west-1/global-rotation", GET)
- .properties(Map.of("endpointId", "eu"))
- .userIdentity(USER_ID),
- "{\"bcpStatus\":{\"rotationStatus\":\"UNKNOWN\"}}",
- 200);
- }
-
- @Test
- void testDeployWithApplicationPackage() {
- // Setup
- addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR));
- deploymentTester.controllerTester().upgradeController(new Version("6.2"));
-
- // POST (deploy) a system application with an application package
- MultiPartStreamer noAppEntity = createApplicationDeployData(Optional.empty());
- tester.assertResponse(request("/application/v4/tenant/hosted-vespa/application/routing/environment/prod/region/us-central-1/instance/default/deploy", POST)
- .data(noAppEntity)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Deployment of system applications during a system upgrade is not allowed\"}",
- 400);
- deploymentTester.controllerTester()
- .upgradeSystem(deploymentTester.controller().readVersionStatus().controllerVersion().get()
- .versionNumber());
- tester.assertResponse(request("/application/v4/tenant/hosted-vespa/application/routing/environment/prod/region/us-central-1/instance/default/deploy", POST)
- .data(noAppEntity)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- new File("deploy-result.json"));
- }
-
-
- @Test
- void testRemovingAllDeployments() {
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .instances("instance1")
- .region("us-west-1")
- .region("us-east-3")
- .region("eu-west-1")
- .endpoint("eu", "default", "eu-west-1")
- .endpoint("default", "default", "us-west-1", "us-east-3")
- .build();
-
- deploymentTester.controllerTester().createTenant("tenant1", ATHENZ_TENANT_DOMAIN.getName(), 432L);
-
- // Create tenant and deploy
- var app = deploymentTester.newDeploymentContext("tenant1", "application1", "instance1");
- app.submit(applicationPackage).deploy();
- tester.controller().jobController().deploy(app.instanceId(), DeploymentContext.devUsEast1, Optional.empty(), applicationPackage);
-
- assertEquals(Set.of(ZoneId.from("prod.us-west-1"), ZoneId.from("prod.us-east-3"), ZoneId.from("prod.eu-west-1"), ZoneId.from("dev.us-east-1")),
- app.instance().deployments().keySet());
-
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deployment", DELETE)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"message\":\"All deployments removed\"}");
-
- assertEquals(Set.of(ZoneId.from("dev.us-east-1")), app.instance().deployments().keySet());
- }
-
- @Test
- void testErrorResponses() {
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
-
- // PUT (update) non-existing tenant returns 403 as tenant access cannot be determined when the tenant does not exist
- tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
- accessDenied,
- 403);
-
- // GET non-existing tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", GET)
- .userIdentity(USER_ID),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'tenant1' does not exist\"}",
- 404);
-
- // GET non-existing tenant's applications
- tester.assertResponse(request("/application/v4/tenant/tenant1/application", GET)
- .userIdentity(USER_ID),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"Tenant 'tenant1' does not exist\"}",
- 404);
-
- // GET non-existing application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", GET)
- .userIdentity(USER_ID),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"tenant1.application1 not found\"}",
- 404);
-
- // GET non-existing deployment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-east/instance/default", GET)
- .userIdentity(USER_ID),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"tenant1.application1 not found\"}",
- 404);
-
- // POST (add) a tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
- .userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("tenant-without-applications.json"));
-
- // POST (add) another tenant under the same domain
- tester.assertResponse(request("/application/v4/tenant/tenant2", POST)
- .userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not create tenant 'tenant2': The Athens domain 'domain1' is already connected to tenant 'tenant1'\"}",
- 400);
-
- // Add the same tenant again
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Tenant 'tenant1' already exists\"}",
- 400);
-
- // POST (add) an Athenz tenant with underscore in name
- tester.assertResponse(request("/application/v4/tenant/my_tenant_2", POST)
- .userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"New tenant or application names must start with a letter, may contain no more than 20 characters, and may only contain lowercase letters, digits or dashes, but no double-dashes.\"}",
- 400);
-
- // POST (add) an Athenz tenant with a reserved name
- tester.assertResponse(request("/application/v4/tenant/hosted-vespa", POST)
- .userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Tenant 'hosted-vespa' already exists\"}",
- 400);
-
- // POST (create) an (empty) application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", POST)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("instance-reference.json"));
-
- // Create the same application again
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", POST)
- .oAuthCredentials(OKTA_CREDENTIALS)
- .userIdentity(USER_ID),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not create 'tenant1.application1.instance1': Instance already exists\"}",
- 400);
-
- ConfigServerMock configServer = tester.serviceRegistry().configServerMock();
- configServer.throwOnNextPrepare(new ConfigServerException(ConfigServerException.ErrorCode.INVALID_APPLICATION_PACKAGE, "Deployment failed", "Invalid application package"));
-
- // GET non-existent application package
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package", GET).userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"no application package has been submitted for tenant1.application1\"}",
- 404);
-
- // GET non-existent application package of specific build
- addScrewdriverUserToDeployRole(SCREWDRIVER_ID, ATHENZ_TENANT_DOMAIN, ApplicationName.from("application1"));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit", POST)
- .screwdriverIdentity(SCREWDRIVER_ID)
- .data(createApplicationSubmissionData(applicationPackageInstance1, 1000)),
- "{\"message\":\"application build 1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\",\"build\":1}");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package", GET)
- .properties(Map.of("build", "42"))
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"No build 42 found for tenant1.application1\"}",
- 404);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/deployment", DELETE).userIdentity(USER_ID).oAuthCredentials(OKTA_CREDENTIALS),
- "{\"message\":\"All deployments removed\"}");
-
- // GET non-existent application package of invalid build
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/package", GET)
- .properties(Map.of("build", "foobar"))
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"invalid value for request parameter 'build': For input string: \\\"foobar\\\"\"}",
- 400);
-
- // POST (deploy) an application to legacy deploy path
- MultiPartStreamer entity = createApplicationDeployData(applicationPackageInstance1);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/dev/region/us-east-1/instance/instance1/deploy", POST)
- .data(entity)
- .userIdentity(USER_ID),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Deployment of tenant1.application1.instance1 is not supported through this API\"}", 400);
-
- // DELETE tenant which has an application
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Could not delete tenant 'tenant1': This tenant has active applications\"}",
- 400);
-
- // DELETE application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", DELETE)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"message\":\"Deleted instance tenant1.application1.instance1\"}");
- // DELETE application again - should produce 404
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", DELETE)
- .oAuthCredentials(OKTA_CREDENTIALS)
- .userIdentity(USER_ID),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"Could not delete instance 'tenant1.application1.instance1': Instance not found\"}",
- 404);
-
- // DELETE and forget an application as non-operator
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE).properties(Map.of("forget", "true"))
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"Only operators can forget a tenant\"}",
- 403);
-
- // DELETE tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"message\":\"Deleted tenant tenant1\"}");
- // DELETE tenant again returns 403 as tenant access cannot be determined when the tenant does not exist
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE)
- .userIdentity(USER_ID),
- accessDenied,
- 403);
-
- // Create legacy tenant name containing underscores
- tester.controller().curator().writeTenant(new AthenzTenant(TenantName.from("my_tenant"), ATHENZ_TENANT_DOMAIN,
- new Property("property1"), Optional.empty(), Optional.empty(), Instant.EPOCH, LastLoginInfo.EMPTY, Instant.EPOCH, List.of()));
-
- // POST (add) a Athenz tenant with dashes duplicates existing one with underscores
- tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
- .userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Tenant 'my-tenant' already exists\"}",
- 400);
- }
-
- @Test
- void testAuthorization() {
- UserId authorizedUser = USER_ID;
- UserId unauthorizedUser = new UserId("othertenant");
-
- // Mutation without an user is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
- "{\n \"message\" : \"Not authenticated\"\n}",
- 401);
-
- // ... but read methods are allowed for authenticated user
- tester.assertResponse(request("/application/v4/tenant/", GET)
- .userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}"),
- "[]",
- 200);
-
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
-
- // Creating a tenant for an Athens domain the user is not admin for is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS)
- .userIdentity(unauthorizedUser),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"The user 'user.othertenant' is not admin in Athenz domain 'domain1'\"}",
- 403);
-
- // (Create it with the right tenant id)
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .userIdentity(authorizedUser)
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("tenant-without-applications.json"),
- 200);
-
- // Creating an application for an Athens domain the user is not admin for is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", POST)
- .userIdentity(unauthorizedUser)
- .oAuthCredentials(OKTA_CREDENTIALS),
- accessDenied,
- 403);
-
- // (Create it with the right tenant id)
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", POST)
- .userIdentity(authorizedUser)
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("instance-reference.json"),
- 200);
-
- // Deploy to an authorized zone by a user tenant is disallowed
- MultiPartStreamer entity = createApplicationDeployData(applicationPackageDefault);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/default/deploy", POST)
- .data(entity)
- .userIdentity(USER_ID),
- accessDenied,
- 403);
-
- // Deleting an application for an Athens domain the user is not admin for is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
- .userIdentity(unauthorizedUser),
- accessDenied,
- 403);
-
- // Create another instance under the application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/default", POST)
- .userIdentity(authorizedUser)
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("instance-reference-default.json"),
- 200);
-
- // (Deleting the application with the right tenant id)
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1", DELETE)
- .userIdentity(authorizedUser)
- .oAuthCredentials(OKTA_CREDENTIALS),
- "{\"message\":\"Deleted application tenant1.application1\"}",
- 200);
-
- // Updating a tenant for an Athens domain the user is not admin for is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .userIdentity(unauthorizedUser),
- accessDenied,
- 403);
-
- // Change Athens domain
- createAthenzDomainWithAdmin(new AthenzDomain("domain2"), USER_ID);
- tester.assertResponse(request("/application/v4/tenant/tenant1", PUT)
- .data("{\"athensDomain\":\"domain2\", \"property\":\"property1\"}")
- .userIdentity(authorizedUser)
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("tenant1.json"),
- 200);
-
- // Deleting a tenant for an Athens domain the user is not admin for is disallowed
- tester.assertResponse(request("/application/v4/tenant/tenant1", DELETE)
- .userIdentity(unauthorizedUser),
- accessDenied,
- 403);
-
- }
-
- @Test
- void athenz_service_must_be_allowed_to_launch_and_be_under_tenant_domain() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .upgradePolicy("default")
- .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("another.domain"), com.yahoo.config.provision.AthenzService.from("service"))
- .region("us-west-1")
- .build();
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
-
- deploymentTester.controllerTester().createTenant("tenant1", ATHENZ_TENANT_DOMAIN.getName(), 1234L);
- var application = deploymentTester.newDeploymentContext("tenant1", "application1", "default");
- ScrewdriverId screwdriverId = new ScrewdriverId("123");
- addScrewdriverUserToDeployRole(screwdriverId, ATHENZ_TENANT_DOMAIN, application.instanceId().application());
-
- allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(new AthenzDomain("another.domain"), "service"));
- // Submit a package with a service under a different Athenz domain from that of the tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit/", POST)
- .data(createApplicationSubmissionData(applicationPackage, 123))
- .screwdriverIdentity(screwdriverId),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Athenz domain in deployment.xml: [another.domain] must match tenant domain: [domain1]\"}",
- 400);
-
- // Set the correct domain in the application package, but do not yet allow Vespa to launch the service.
- applicationPackage = new ApplicationPackageBuilder()
- .upgradePolicy("default")
- .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service"))
- .region("us-west-1")
- .build();
-
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit", POST)
- .data(createApplicationSubmissionData(applicationPackage, 123))
- .screwdriverIdentity(screwdriverId),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Not allowed to launch Athenz service domain1.service\"}",
- 400);
-
- // Allow Vespa to launch the Athenz service.
- allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"));
-
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit/", POST)
- .data(createApplicationSubmissionData(applicationPackage, 123))
- .screwdriverIdentity(screwdriverId),
- "{\"message\":\"application build 1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z\",\"build\":1}");
- }
-
- @Test
- void personal_deployment_with_athenz_service_requires_user_is_admin() {
- // Setup
- UserId tenantAdmin = new UserId("tenant-admin");
- UserId userId = new UserId("new-user");
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, tenantAdmin);
- allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"));
-
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service"))
- .build();
-
- createTenantAndApplication();
- MultiPartStreamer entity = createApplicationDeployData(applicationPackage);
- // POST (deploy) an application to dev through a deployment job, with user instance and a proper tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/new-user/deploy/dev-us-east-1", POST)
- .data(entity)
- .userIdentity(userId),
- accessDenied,
- 403);
-
- // Add "new-user" to the admin role, to allow service launches.
- tester.athenzClientFactory().getSetup()
- .domains.get(ATHENZ_TENANT_DOMAIN)
- .admin(HostedAthenzIdentities.from(userId));
-
- // POST (deploy) an application to dev through a deployment job
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/new-user/deploy/dev-us-east-1", POST)
- .data(entity)
- .userIdentity(userId),
- "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for tenant1.application1.new-user. This may take about 15 minutes the first time.\",\"run\":1}");
- }
-
- // Deploy to sandbox tenant launching a service from another domain.
- @Test
- void developers_can_deploy_when_privileged() {
- // Create an athenz domain where the developer is not yet authorized
- UserId tenantAdmin = new UserId("tenant-admin");
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, tenantAdmin);
- allowLaunchOfService(new com.yahoo.vespa.athenz.api.AthenzService(ATHENZ_TENANT_DOMAIN, "service"));
-
- // Create the sandbox tenant and authorize the developer
- UserId developer = new UserId("developer");
- AthenzDomain sandboxDomain = new AthenzDomain("sandbox");
- createAthenzDomainWithAdmin(sandboxDomain, developer);
- AthenzTenantSpec tenantSpec = new AthenzTenantSpec(TenantName.from("sandbox"),
- sandboxDomain,
- new Property("vespa"),
- Optional.empty());
- AthenzCredentials credentials = new AthenzCredentials(
- new AthenzPrincipal(new AthenzUser(developer.id())), sandboxDomain, OKTA_CREDENTIALS);
- tester.controller().tenants().create(tenantSpec, credentials);
- tester.controller().applications().createApplication(TenantAndApplicationId.from("sandbox", "myapp"), credentials);
-
- // Create an application package referencing the service from the other domain
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain1"), com.yahoo.config.provision.AthenzService.from("service"))
- .build();
-
- // deploy the application to a dev zone. Should fail since the developer is not authorized to launch the service
- MultiPartStreamer entity = createApplicationDeployData(applicationPackage);
- tester.assertResponse(request("/application/v4/tenant/sandbox/application/myapp/instance/default/deploy/dev-us-east-1", POST)
- .data(entity)
- .userIdentity(developer),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"User user.developer is not allowed to launch service domain1.service. Please reach out to the domain admin.\"}",
- 400);
-
- // Allow developer launch privilege to domain1.service. Deployment now completes.
- AthenzDbMock.Domain domainMock = tester.athenzClientFactory().getSetup().getOrCreateDomain(ATHENZ_TENANT_DOMAIN);
- domainMock.withPolicy("launch-" + developer.id(), "user." + developer.id(), "launch", "service.service");
-
-
- tester.assertResponse(request("/application/v4/tenant/sandbox/application/myapp/instance/default/deploy/dev-us-east-1", POST)
- .data(entity)
- .userIdentity(developer),
- "{\"message\":\"Deployment started in run 1 of dev-us-east-1 for sandbox.myapp. This may take about 15 minutes the first time.\",\"run\":1}",
- 200);
-
- // To add temporary support allowing tenant admins to launch services
- UserId developer2 = new UserId("developer2");
- // to be able to deploy to sandbox tenant
- tester.athenzClientFactory().getSetup().getOrCreateDomain(sandboxDomain).tenantAdmin(new AthenzUser(developer2.id()));
- tester.athenzClientFactory().getSetup().getOrCreateDomain(ATHENZ_TENANT_DOMAIN).tenantAdmin(new AthenzUser(developer2.id()));
- tester.assertResponse(request("/application/v4/tenant/sandbox/application/myapp/instance/default/deploy/dev-us-east-1", POST)
- .data(entity)
- .userIdentity(developer2),
- "{\"message\":\"Deployment started in run 2 of dev-us-east-1 for sandbox.myapp. This may take about 15 minutes the first time.\",\"run\":2}",
- 200);
-
-
- // POST (deploy) an application package as content type application/zip — not multipart
- tester.assertResponse(request("/application/v4/tenant/sandbox/application/myapp/instance/default/deploy/dev-us-east-1", POST)
- .data(applicationPackageInstance1.zippedContent())
- .contentType("application/zip")
- .userIdentity(developer2),
- "{\"message\":\"Deployment started in run 3 of dev-us-east-1 for sandbox.myapp. This may take about 15 minutes the first time.\",\"run\":3}");
-
- // POST (deploy) an application package not as content type application/zip — not multipart — is disallowed
- tester.assertResponse(request("/application/v4/tenant/sandbox/application/myapp/instance/default/deploy/dev-us-east-1", POST)
- .data(applicationPackageInstance1.zippedContent())
- .contentType("application/gzip")
- .userIdentity(developer2),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Expected a multipart or application/zip message, but got Content-Type: application/gzip\"}", 400);
- }
-
- @Test
- void applicationWithRoutingPolicy() {
- var app = deploymentTester.newDeploymentContext(createTenantAndApplication());
- var zone = ZoneId.from(Environment.prod, RegionName.from("us-west-1"));
- deploymentTester.controllerTester().zoneRegistry().setRoutingMethod(ZoneApiMock.from(zone),
- RoutingMethod.exclusive);
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain"), AthenzService.from("service"))
- .instances("instance1")
- .region(zone.region().value())
- .build();
- app.submit(applicationPackage).deploy();
-
- // GET application
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", GET)
- .userIdentity(USER_ID),
- new File("instance-with-routing-policy.json"));
-
- // GET deployment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/instance1", GET)
- .userIdentity(USER_ID),
- new File("deployment-with-routing-policy.json"));
-
- // GET deployment
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/environment/prod/region/us-west-1/instance/instance1", GET)
- .userIdentity(USER_ID),
- new File("deployment-without-shared-endpoints.json"));
- }
-
- @Test
- void support_access() {
- var app = deploymentTester.newDeploymentContext(createTenantAndApplication());
- var zone = ZoneId.from(Environment.prod, RegionName.from("us-west-1"));
- deploymentTester.controllerTester().zoneRegistry().setRoutingMethod(ZoneApiMock.from(zone), RoutingMethod.exclusive);
- addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR));
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .athenzIdentity(com.yahoo.config.provision.AthenzDomain.from("domain"), AthenzService.from("service"))
- .instances("instance1")
- .region(zone.region().value())
- .build();
- app.submit(applicationPackage).deploy();
-
- // GET support access status returns no history
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/access/support", GET)
- .userIdentity(USER_ID),
- "{\"state\":{\"supportAccess\":\"NOT_ALLOWED\"},\"history\":[],\"grants\":[]}", 200
- );
-
- // POST allowing support access adds to history
- Instant now = tester.controller().clock().instant().truncatedTo(ChronoUnit.SECONDS);
- String allowedResponse = "{\"state\":{\"supportAccess\":\"ALLOWED\",\"until\":\"" + serializeInstant(now.plus(7, ChronoUnit.DAYS))
- + "\",\"by\":\"user.myuser\"},\"history\":[{\"state\":\"allowed\",\"at\":\"" + serializeInstant(now)
- + "\",\"until\":\"" + serializeInstant(now.plus(7, ChronoUnit.DAYS))
- + "\",\"by\":\"user.myuser\"}],\"grants\":[]}";
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/access/support", POST)
- .userIdentity(USER_ID),
- allowedResponse, 200
- );
-
- // Grant access to support user
- X509Certificate support_cert = grantCertificate(now, now.plusSeconds(3600));
- String grantPayload = "{\n" +
- " \"applicationId\": \"tenant1:application1:instance1\",\n" +
- " \"zone\": \"prod.us-west-1\",\n" +
- " \"certificate\":\"" + X509CertificateUtils.toPem(support_cert) + "\"\n" +
- "}";
- tester.assertResponse(request("/controller/v1/access/grants/" + HOSTED_VESPA_OPERATOR.id(), POST)
- .data(grantPayload)
- .userIdentity(HOSTED_VESPA_OPERATOR),
- "{\"message\":\"Operator user.johnoperator granted access and job production-us-west-1 triggered\"}");
-
- // GET shows grant
- String grantResponse = allowedResponse.replaceAll("\"grants\":\\[]",
- "\"grants\":[{\"requestor\":\"user.johnoperator\",\"notBefore\":\"" + serializeInstant(now) + "\",\"notAfter\":\"" + serializeInstant(now.plusSeconds(3600)) + "\"}]");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/access/support", GET)
- .userIdentity(USER_ID),
- grantResponse, 200
- );
-
- // Should be 1 available grant
- tester.serviceRegistry().clock().advance(Duration.ofSeconds(1));
- now = tester.serviceRegistry().clock().instant();
- List<SupportAccessGrant> activeGrants = tester.controller().supportAccess().activeGrantsFor(new DeploymentId(ApplicationId.fromSerializedForm("tenant1:application1:instance1"), zone));
- assertEquals(1, activeGrants.size());
-
- // Adding grant should trigger job
- app.assertRunning(DeploymentContext.productionUsWest1);
-
- // DELETE removes access
- String disallowedResponse = grantResponse
- .replaceAll("ALLOWED\".*?}", "NOT_ALLOWED\"}")
- .replace("history\":[", "history\":[{\"state\":\"disallowed\",\"at\":\"" + serializeInstant(now) + "\",\"by\":\"user.myuser\"},");
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/access/support", DELETE)
- .userIdentity(USER_ID),
- disallowedResponse, 200
- );
-
- // Revoking access should trigger job
- app.assertRunning(DeploymentContext.productionUsWest1);
-
- // Should be no available grant
- activeGrants = tester.controller().supportAccess().activeGrantsFor(new DeploymentId(ApplicationId.fromSerializedForm("tenant1:application1:instance1"), zone));
- assertEquals(0, activeGrants.size());
- }
-
- @Test
- void create_application_on_deploy_with_okta() {
- // Setup
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
- addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR));
-
- // Create tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST).userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("tenant-without-applications.json"));
-
- // Deploy application
- var id = ApplicationId.from("tenant1", "application1", "instance1");
- var appId = TenantAndApplicationId.from(id);
- var entity = createApplicationDeployData(applicationPackageInstance1);
-
- assertTrue(tester.controller().applications().getApplication(appId).isEmpty());
-
- // POST (deploy) an application to start a manual deployment to dev
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploy/dev-us-east-1/", POST)
- .data(entity)
- .oAuthCredentials(OKTA_CREDENTIALS)
- .userIdentity(USER_ID),
- """
- {"message":"Deployment started in run 1 of dev-us-east-1 for tenant1.application1.instance1. This may take about 15 minutes the first time.","run":1}""");
-
- assertTrue(tester.controller().applications().getApplication(appId).isPresent());
- }
-
- @Test
- void create_application_on_deploy_with_athenz() {
- // Setup
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
- addUserToHostedOperatorRole(HostedAthenzIdentities.from(HOSTED_VESPA_OPERATOR));
-
- // Create tenant
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST).userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("tenant-without-applications.json"));
-
- // Deploy application
- var id = ApplicationId.from("tenant1", "application1", "instance1");
- var appId = TenantAndApplicationId.from(id);
- var entity = createApplicationDeployData(applicationPackageInstance1);
-
- assertTrue(tester.controller().applications().getApplication(appId).isEmpty());
-
- // POST (deploy) an application to start a manual deployment to dev
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1/deploy/dev-us-east-1/", POST)
- .data(entity)
- .userIdentity(USER_ID),
- """
- {"error-code":"BAD_REQUEST","message":"Application does not exist. Create application in Console first."}""", 400);
-
- assertFalse(tester.controller().applications().getApplication(appId).isPresent());
- }
-
- @Test
- void only_build_job_can_submit() {
- createTenantAndApplication();
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit/", POST)
- .data(createApplicationSubmissionData(applicationPackageDefault, SCREWDRIVER_ID.value()))
- .userIdentity(USER_ID),
- accessDenied,
- 403);
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/submit/", POST)
- .data(createApplicationSubmissionData(applicationPackageDefault, SCREWDRIVER_ID.value()))
- .screwdriverIdentity(SCREWDRIVER_ID),
- """
- {"message":"application build 1, source revision of repository 'repository1', branch 'master' with commit 'commit1', by a@b, built against 6.1 at 1970-01-01T00:00:01Z","build":1}""",
- 200);
- }
-
- private static String serializeInstant(Instant i) {
- return DateTimeFormatter.ISO_INSTANT.format(i.truncatedTo(ChronoUnit.SECONDS));
- }
-
- static X509Certificate grantCertificate(Instant notBefore, Instant notAfter) {
- return X509CertificateBuilder
- .fromKeypair(
- KeyUtils.generateKeypair(KeyAlgorithm.EC, 256), new X500Principal("CN=mysubject"),
- notBefore, notAfter, SignatureAlgorithm.SHA256_WITH_ECDSA, BigInteger.valueOf(1))
- .build();
- }
-
- private MultiPartStreamer createApplicationDeployData(ApplicationPackage applicationPackage) {
- return createApplicationDeployData(Optional.of(applicationPackage));
- }
-
- private MultiPartStreamer createApplicationDeployData(Optional<ApplicationPackage> applicationPackage) {
- return createApplicationDeployData(applicationPackage, Optional.empty());
- }
-
- private MultiPartStreamer createApplicationDeployData(Optional<ApplicationPackage> applicationPackage,
- Optional<ApplicationVersion> applicationVersion) {
- MultiPartStreamer streamer = new MultiPartStreamer();
- streamer.addJson("deployOptions", deployOptions(applicationVersion));
- applicationPackage.ifPresent(ap -> streamer.addBytes("applicationZip", ap.zippedContent()));
- return streamer;
- }
-
- static MultiPartStreamer createApplicationSubmissionData(ApplicationPackage applicationPackage, long projectId) {
- return new MultiPartStreamer().addJson(EnvironmentResource.SUBMIT_OPTIONS, "{\"repository\":\"repository1\",\"branch\":\"master\",\"commit\":\"commit1\","
- + "\"projectId\":" + projectId + ",\"authorEmail\":\"a@b\","
- + "\"description\":\"my best commit yet\",\"risk\":9001}")
- .addBytes(EnvironmentResource.APPLICATION_ZIP, applicationPackage.zippedContent())
- .addBytes(EnvironmentResource.APPLICATION_TEST_ZIP, testZip);
- }
-
- private String deployOptions(Optional<ApplicationVersion> applicationVersion) {
- return "{\"vespaVersion\":null," +
- "\"ignoreValidationErrors\":false" +
- applicationVersion.map(version ->
- "," +
- "\"buildNumber\":" + version.buildNumber() + "," +
- "\"sourceRevision\":{" +
- "\"repository\":\"" + version.source().get().repository() + "\"," +
- "\"branch\":\"" + version.source().get().branch() + "\"," +
- "\"commit\":\"" + version.source().get().commit() + "\"" +
- "}"
- ).orElse("") +
- "}";
- }
-
- /** Make a request with (athens) user domain1.mytenant */
- private RequestBuilder request(String path, Request.Method method) {
- return new RequestBuilder(path, method);
- }
-
- /**
- * In production this happens outside hosted Vespa, so there is no API for it and we need to reach down into the
- * mock setup to replicate the action.
- */
- private void createAthenzDomainWithAdmin(AthenzDomain domain, UserId userId) {
- AthenzDbMock.Domain domainMock = tester.athenzClientFactory().getSetup().getOrCreateDomain(domain);
- domainMock.markAsVespaTenant();
- domainMock.admin(AthenzUser.fromUserId(userId.id()));
- }
-
- /**
- * Mock athenz service identity configuration. Simulates that configserver is allowed to launch a service
- */
- private void allowLaunchOfService(com.yahoo.vespa.athenz.api.AthenzService service) {
- AthenzDbMock.Domain domainMock = tester.athenzClientFactory().getSetup().getOrCreateDomain(service.getDomain());
- String principalRegex = tester.controller().zoneRegistry().accessControlDomain().value() + ".provider.*";
- domainMock.withPolicy("provider-launch", principalRegex,"launch", "service." + service.getName());
- }
-
- /**
- * In production this happens outside hosted Vespa, so there is no API for it and we need to reach down into the
- * mock setup to replicate the action.
- */
- private void addScrewdriverUserToDeployRole(ScrewdriverId screwdriverId,
- AthenzDomain domain,
- ApplicationName application) {
- tester.authorize(domain, HostedAthenzIdentities.from(screwdriverId), ApplicationAction.deploy, application);
- }
-
- private ApplicationId createTenantAndApplication() {
- createAthenzDomainWithAdmin(ATHENZ_TENANT_DOMAIN, USER_ID);
- tester.assertResponse(request("/application/v4/tenant/tenant1", POST)
- .userIdentity(USER_ID)
- .data("{\"athensDomain\":\"domain1\", \"property\":\"property1\"}")
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("tenant-without-applications.json"));
- tester.assertResponse(request("/application/v4/tenant/tenant1/application/application1/instance/instance1", POST)
- .userIdentity(USER_ID)
- .oAuthCredentials(OKTA_CREDENTIALS),
- new File("instance-reference.json"));
- addScrewdriverUserToDeployRole(SCREWDRIVER_ID, ATHENZ_TENANT_DOMAIN, ApplicationName.from("application1"));
-
- return ApplicationId.from("tenant1", "application1", "instance1");
- }
-
- /**
- * Cluster info, utilization and application and deployment metrics are maintained async by maintainers.
- *
- * This sets these values as if the maintainers has been ran.
- */
- private void setDeploymentMaintainedInfo() {
- for (Application application : deploymentTester.applications().asList()) {
- deploymentTester.applications().lockApplicationOrThrow(application.id(), lockedApplication -> {
- lockedApplication = lockedApplication.with(new ApplicationMetrics(0.5, 0.7));
-
- for (Instance instance : application.instances().values()) {
- for (Deployment deployment : instance.deployments().values()) {
- DeploymentMetrics metrics = new DeploymentMetrics(1, 2, 3, 4, 5,
- Optional.of(Instant.ofEpochMilli(123123)), Map.of());
- lockedApplication = lockedApplication.with(instance.name(),
- lockedInstance -> lockedInstance.with(deployment.zone(), metrics)
- .recordActivityAt(Instant.parse("2018-06-01T10:15:30.00Z"), deployment.zone()));
- }
- deploymentTester.applications().store(lockedApplication);
- }
- });
- }
- }
-
- private void updateContactInformation() {
- Contact contact = new Contact(URI.create("www.contacts.tld/1234"),
- URI.create("www.properties.tld/1234"),
- URI.create("www.issues.tld/1234"),
- List.of(List.of("alice"), List.of("bob")), "queue", Optional.empty());
- tester.controller().tenants().lockIfPresent(TenantName.from("tenant2"),
- LockedTenant.Athenz.class,
- lockedTenant -> tester.controller().tenants().store(lockedTenant.with(contact)));
- }
-
- private void registerContact(long propertyId) {
- PropertyId p = new PropertyId(String.valueOf(propertyId));
- tester.serviceRegistry().contactRetrieverMock().addContact(p, new Contact(URI.create("www.issues.tld/" + p.id()),
- URI.create("www.contacts.tld/" + p.id()),
- URI.create("www.properties.tld/" + p.id()),
- List.of(Collections.singletonList("alice"),
- Collections.singletonList("bob")),
- "queue", Optional.empty()));
- }
-
- private void updateMetrics() {
- tester.serviceRegistry().configServerMock().setProtonMetrics(List.of(
- (new SearchNodeMetrics("content/doc/"))
- .addMetric(SearchNodeMetrics.DOCUMENTS_ACTIVE_COUNT, 11430)
- .addMetric(SearchNodeMetrics.DOCUMENTS_READY_COUNT, 11430)
- .addMetric(SearchNodeMetrics.DOCUMENTS_TOTAL_COUNT, 11430)
- .addMetric(SearchNodeMetrics.DOCUMENT_DISK_USAGE, 44021)
- .addMetric(SearchNodeMetrics.RESOURCE_DISK_USAGE_AVERAGE, 0.0168421)
- .addMetric(SearchNodeMetrics.RESOURCE_MEMORY_USAGE_AVERAGE, 0.103482),
- (new SearchNodeMetrics("content/music/"))
- .addMetric(SearchNodeMetrics.DOCUMENTS_ACTIVE_COUNT, 32210)
- .addMetric(SearchNodeMetrics.DOCUMENTS_READY_COUNT, 32000)
- .addMetric(SearchNodeMetrics.DOCUMENTS_TOTAL_COUNT, 32210)
- .addMetric(SearchNodeMetrics.DOCUMENT_DISK_USAGE, 90113)
- .addMetric(SearchNodeMetrics.RESOURCE_DISK_USAGE_AVERAGE, 0.23912)
- .addMetric(SearchNodeMetrics.RESOURCE_MEMORY_USAGE_AVERAGE, 0.00912)
- ));
- }
-
- private void addNotifications(TenantName tenantName) {
- tester.controller().notificationsDb().setApplicationPackageNotification(
- NotificationSource.from(TenantAndApplicationId.from(tenantName.value(), "app1")),
- List.of("Something something deprecated..."));
- tester.controller().notificationsDb().setDeploymentNotification(
- new RunId(ApplicationId.from(tenantName.value(), "app2", "instance1"), DeploymentContext.systemTest, 12),
- "Failed to deploy: Node allocation failure");
- }
-
- private void assertGlobalRouting(DeploymentId deployment, RoutingStatus.Value value, RoutingStatus.Agent agent) {
- Instant changedAt = tester.controller().clock().instant();
- DeploymentRoutingContext context = tester.controller().routing().of(deployment);
- RoutingStatus status = context.routingStatus();
- assertEquals(value, status.value());
- assertEquals(agent, status.agent());
- assertEquals(changedAt, status.changedAt());
- }
-
- private static class RequestBuilder implements Supplier<Request> {
-
- private final String path;
- private final Request.Method method;
- private byte[] data = new byte[0];
- private AthenzIdentity identity;
- private OAuthCredentials oAuthCredentials;
- private String contentType = "application/json";
- private final Map<String, List<String>> headers = new HashMap<>();
- private final Map<String, String> properties = new HashMap<>();
-
- private RequestBuilder(String path, Request.Method method) {
- this.path = path;
- this.method = method;
- }
-
- private RequestBuilder data(byte[] data) { this.data = data; return this; }
- private RequestBuilder data(String data) { return data(data.getBytes(UTF_8)); }
- private RequestBuilder data(MultiPartStreamer streamer) {
- return Exceptions.uncheck(() -> data(streamer.data().readAllBytes()).contentType(streamer.contentType()));
- }
-
- private RequestBuilder userIdentity(UserId userId) { this.identity = HostedAthenzIdentities.from(userId); return this; }
- private RequestBuilder screwdriverIdentity(ScrewdriverId screwdriverId) { this.identity = HostedAthenzIdentities.from(screwdriverId); return this; }
- private RequestBuilder oAuthCredentials(OAuthCredentials oAuthCredentials) { this.oAuthCredentials = oAuthCredentials; return this; }
- private RequestBuilder contentType(String contentType) { this.contentType = contentType; return this; }
- private RequestBuilder recursive(String recursive) {return properties(Map.of("recursive", recursive)); }
- private RequestBuilder properties(Map<String, String> properties) { this.properties.putAll(properties); return this; }
- private RequestBuilder header(String name, String value) {
- this.headers.putIfAbsent(name, new ArrayList<>());
- this.headers.get(name).add(value);
- return this;
- }
-
- @Override
- public Request get() {
- Request request = new Request("http://localhost:8080" + path +
- properties.entrySet().stream()
- .map(entry -> encode(entry.getKey(), UTF_8) + "=" + encode(entry.getValue(), UTF_8))
- .collect(joining("&", "?", "")),
- data, method);
- request.getHeaders().addAll(headers);
- request.getHeaders().put("Content-Type", contentType);
- // user and domain parameters are translated to a Principal by MockAuthorizer as we do not run HTTP filters
- if (identity != null) addIdentityToRequest(request, identity);
- if (oAuthCredentials != null) addOAuthCredentials(request, oAuthCredentials);
- return request;
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/CliApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/CliApiHandlerTest.java
deleted file mode 100644
index 9c169213b58..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/CliApiHandlerTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-/**
- * @author mpolden
- */
-public class CliApiHandlerTest extends ControllerContainerTest {
-
- private ContainerTester tester;
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, "src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/");
- }
-
- @Test
- void root() {
- tester.assertResponse(authenticatedRequest("http://localhost:8080/cli/v1/"), "{\"minVersion\":\"7.547.18\"}");
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java
deleted file mode 100644
index f9ba5850d2d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/JobControllerApiHandlerHelperTest.java
+++ /dev/null
@@ -1,244 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.HttpResponse;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RevisionId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.RunId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.TestReport;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayOutputStream;
-import java.net.URI;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-
-import static com.yahoo.vespa.hosted.controller.api.integration.configserver.ConfigServerException.ErrorCode.INVALID_APPLICATION_PACKAGE;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.applicationPackage;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.devAwsUsEast2a;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.devUsEast1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsCentral1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsEast3;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.stagingTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.systemTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.testUsCentral1;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.installationFailed;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.invalidApplication;
-import static com.yahoo.vespa.hosted.controller.deployment.RunStatus.running;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author jonmv
- * @author freva
- */
-public class JobControllerApiHandlerHelperTest {
-
- @Test
- void testResponses() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .stagingTest()
- .blockChange(true, true, "mon,tue", "7-13", "UTC")
- .blockChange(false, true, "sun", "0-23", "CET")
- .blockChange(true, false, "fri-sat", "8", "America/Los_Angeles")
- .region("us-central-1")
- .test("us-central-1")
- .parallel("us-west-1", "us-east-3")
- .build();
- DeploymentTester tester = new DeploymentTester();
- var app = tester.newDeploymentContext();
- tester.clock().setInstant(Instant.EPOCH);
-
- // All completed runs will have a test report.
- tester.cloud().testReport(TestReport.fromJson("{\"summary\":{\"success\": 1, \"failed\": 0}}"));
-
- // Revision 1 gets deployed everywhere.
- app.submit(applicationPackage).deploy();
- RevisionId revision1 = app.lastSubmission().get();
- assertEquals(1000, tester.application().projectId().getAsLong());
- // System test includes test report
- assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), systemTest).get().id(), "0"), "system-test-log.json");
-
- tester.clock().advance(Duration.ofMillis(1000));
- // Revision 2 gets deployed everywhere except in us-east-3.
- RevisionId revision2 = app.submit(applicationPackage).lastSubmission().get();
- app.runJob(systemTest);
- app.runJob(stagingTest);
- app.runJob(productionUsCentral1);
- app.runJob(testUsCentral1);
-
- tester.triggerJobs();
-
- // us-east-3 eats the deployment failure and fails before deployment, while us-west-1 fails after.
- tester.configServer().throwOnNextPrepare(new ConfigServerException(INVALID_APPLICATION_PACKAGE, "ERROR!", "Failed to deploy application"));
- tester.runner().run();
- assertEquals(invalidApplication, tester.jobs().last(app.instanceId(), productionUsEast3).get().status());
-
- tester.runner().run();
- tester.clock().advance(Duration.ofHours(4).plusSeconds(1));
- tester.runner().run();
- assertEquals(installationFailed, tester.jobs().last(app.instanceId(), productionUsWest1).get().status());
- assertEquals(revision2, app.deployment(productionUsCentral1.zone()).revision());
- assertEquals(revision1, app.deployment(productionUsEast3.zone()).revision());
- assertEquals(revision2, app.deployment(productionUsWest1.zone()).revision());
-
- tester.clock().advance(Duration.ofMillis(1000));
-
- // Revision 3 starts.
- app.submit(applicationPackage)
- .runJob(systemTest).runJob(stagingTest);
- tester.triggerJobs(); // Starts runs for us-central-1 and a new staging test run.
- tester.runner().run();
- assertEquals(running, tester.jobs().last(app.instanceId(), productionUsCentral1).get().status());
- assertEquals(running, tester.jobs().last(app.instanceId(), stagingTest).get().status());
-
- // Staging deployment expires and the job fails, and is immediately retried.
- tester.controller().applications().deactivate(app.instanceId(), stagingTest.zone());
- tester.runner().run();
- assertEquals(installationFailed, tester.jobs().last(app.instanceId(), stagingTest).get().status());
-
- // Staging deployment expires again, the job fails for the second time, and won't be retried immediately.
- tester.clock().advance(Duration.ofMillis(100_000)); // Advance time to avoid immediate retry
- tester.triggerJobs();
- tester.runner().run();
- assertEquals(running, tester.jobs().last(app.instanceId(), stagingTest).get().status());
- tester.controller().applications().deactivate(app.instanceId(), stagingTest.zone());
- tester.runner().run();
- assertEquals(installationFailed, tester.jobs().last(app.instanceId(), stagingTest).get().status());
-
- tester.triggerJobs();
- assertEquals(installationFailed, tester.jobs().last(app.instanceId(), stagingTest).get().status());
-
- // System upgrades to a new version, which won't yet start.
- Version platform = new Version("7.1");
- tester.controllerTester().upgradeSystem(platform);
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- // us-central-1 has started, deployed, and is installing. Deployment is not yet verified.
- // us-east-3 is waiting for the failed staging test and us-central-1, while us-west-1 is waiting only for us-central-1.
- // Only us-east-3 is verified, on revision1.
- // staging-test has 5 runs: one success without sources on revision1, one success from revision1 to revision2,
- // one success from revision2 to revision3 and two failures from revision1 to revision3.
- assertResponse(JobControllerApiHandlerHelper.runResponse(tester.controller(), new JobId(app.instanceId(), stagingTest), Optional.empty(), URI.create("https://some.url:43/root")), "staging-runs.json");
- assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), stagingTest).get().id(), "0"), "staging-test-log.json");
- assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), productionUsEast3).get().id(), "0"), "us-east-3-log-without-first.json");
- assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root/")), "overview.json");
-
- var userApp = tester.newDeploymentContext(app.instanceId().tenant().value(), app.instanceId().application().value(), "user");
- userApp.runJob(devAwsUsEast2a, applicationPackage);
- assertResponse(JobControllerApiHandlerHelper.runResponse(tester.controller(), new JobId(userApp.instanceId(), devAwsUsEast2a), Optional.empty(), URI.create("https://some.url:43/root")), "dev-aws-us-east-2a-runs.json");
- assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), userApp.instanceId(), URI.create("https://some.url:43/root/")), "overview-user-instance.json");
- assertResponse(JobControllerApiHandlerHelper.overviewResponse(tester.controller(), app.application().id(), URI.create("https://some.url:43/root/")), "deployment-overview-2.json");
-
- tester.configServer().setLogStream(() -> "no more logs");
- assertResponse(JobControllerApiHandlerHelper.vespaLogsResponse(tester.jobs(), new RunId(app.instanceId(), stagingTest, 1), 0, false), "vespa.log");
- assertResponse(JobControllerApiHandlerHelper.vespaLogsResponse(tester.jobs(), new RunId(app.instanceId(), stagingTest, 1), 0, true), "vespa.log");
- }
-
- @Test
- void testDevResponses() {
- DeploymentTester tester = new DeploymentTester();
- var app = tester.newDeploymentContext();
- tester.clock().setInstant(Instant.EPOCH);
-
- ZoneId zone = DeploymentContext.devUsEast1.zone();
- tester.jobs().deploy(app.instanceId(), DeploymentContext.devUsEast1, Optional.empty(), applicationPackage());
- tester.configServer().setLogStream(() -> "1554970337.935104\t17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\t5480\tcontainer\tstdout\tinfo\tERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)\n");
- assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), devUsEast1).get().id(), null), "dev-us-east-1-log-first-part.json");
-
- tester.configServer().setLogStream(() -> "Nope, this won't be logged");
- tester.configServer().convergeServices(app.instanceId(), zone);
- tester.runner().run();
- assertResponse(JobControllerApiHandlerHelper.runDetailsResponse(tester.jobs(), tester.jobs().last(app.instanceId(), devUsEast1).get().id(), "8"), "dev-us-east-1-log-second-part.json");
-
- tester.jobs().deploy(app.instanceId(), DeploymentContext.devUsEast1, Optional.empty(), applicationPackage());
- assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root")), "dev-overview.json");
- }
-
- @Test
- void testResponsesWithDirectDeployment() {
- var tester = new DeploymentTester();
- var app = tester.newDeploymentContext();
- tester.clock().setInstant(Instant.EPOCH);
- var region = "us-west-1";
- var applicationPackage = new ApplicationPackageBuilder().region(region).build();
- // Deploy directly to production zone, like integration tests.
- tester.controller().jobController().deploy(tester.instance().id(), productionUsWest1, Optional.empty(), applicationPackage);
- assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root/")),
- "jobs-direct-deployment.json");
- }
-
- @Test
- void testResponsesWithDryRunDeployment() {
- var tester = new DeploymentTester();
- var app = tester.newDeploymentContext();
- tester.clock().setInstant(Instant.EPOCH);
- var region = "us-west-1";
- var applicationPackage = new ApplicationPackageBuilder().region(region).build();
- // Deploy directly to production zone, like integration tests, with dryRun.
- tester.controller().jobController().deploy(tester.instance().id(), productionUsWest1, Optional.empty(), applicationPackage, true, true);
- assertResponse(JobControllerApiHandlerHelper.jobTypeResponse(tester.controller(), app.instanceId(), URI.create("https://some.url:43/root/")),
- "jobs-direct-deployment.json");
- }
-
- @Test
- void testEnclave() {
- var cloudAccount = CloudAccount.from("aws:123456789012");
- var applicationPackage = new ApplicationPackageBuilder()
- .cloudAccount(cloudAccount.value())
- .stagingTest()
- .systemTest()
- .region("aws-us-east-1c")
- .build();
- var tester = new DeploymentTester(new ControllerTester(SystemName.Public));
- tester.controllerTester().flagSource().withListFlag(PermanentFlags.CLOUD_ACCOUNTS.id(), List.of(cloudAccount.value()), String.class);
- tester.controllerTester().zoneRegistry().configureCloudAccount(cloudAccount, systemTest.zone(), stagingTest.zone(), ZoneId.from("prod.aws-us-east-1c"));
-
- var app = tester.newDeploymentContext();
- app.submit(applicationPackage).deploy();
- assertEquals(Optional.of(cloudAccount), tester.controllerTester().configServer().cloudAccount(app.deploymentIdIn(systemTest.zone())));
-
- assertResponse(JobControllerApiHandlerHelper.overviewResponse(tester.controller(), app.application().id(), URI.create("https://some.url:43/root/")), "overview-enclave.json");
- }
-
- private void assertResponse(HttpResponse response, String fileName) {
- try {
- Path path = Paths.get("src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/").resolve(fileName);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- response.render(baos);
- if (fileName.endsWith(".json")) {
- byte[] actualJson = SlimeUtils.toJsonBytes(SlimeUtils.jsonToSlimeOrThrow(baos.toByteArray()).get(), false);
- // Files.write(path, actualJson);
- byte[] expected = Files.readAllBytes(path);
- assertEquals(new String(SlimeUtils.toJsonBytes(SlimeUtils.jsonToSlimeOrThrow(expected).get(), false), UTF_8),
- new String(actualJson, UTF_8));
- }
- else {
- assertEquals(Files.readString(path),
- baos.toString(UTF_8));
- }
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParserTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParserTest.java
deleted file mode 100644
index 2a1caafe1ec..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/MultipartParserTest.java
+++ /dev/null
@@ -1,116 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.application;
-
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.jdisc.Container;
-import com.yahoo.jdisc.Request;
-import com.yahoo.jdisc.ResourceReference;
-import com.yahoo.jdisc.handler.RequestHandler;
-import com.yahoo.jdisc.service.CurrentContainer;
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayInputStream;
-import java.net.URI;
-import java.nio.charset.StandardCharsets;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author bratseth
- */
-public class MultipartParserTest {
-
- @Test
- void parser() {
- String data =
- "Content-Type: multipart/form-data; boundary=AaB03x\r\n" +
- "\r\n" +
- "--AaB03x\r\n" +
- "Content-Disposition: form-data; name=\"submit-name\"\r\n" +
- "\r\n" +
- "Larry\r\n" +
- "--AaB03x\r\n" +
- "Content-Disposition: form-data; name=\"submit-address\"\r\n" +
- "Content-Type: text/plain\r\n" +
- "\r\n" +
- "House 1\r\n" +
- "--AaB03x\r\n" +
- "Content-Disposition: form-data; name=\"files\"; filename=\"file1.txt\"\r\n" +
- "Content-Type: text/plain\r\n" +
- "\r\n" +
- "... contents of file1.txt ...\r\n" +
- "--AaB03x--\r\n";
- Map<String, byte[]> parts = parse(data, Long.MAX_VALUE);
- assertEquals(3, parts.size());
- assertTrue(parts.containsKey("submit-name"));
- assertTrue(parts.containsKey("submit-address"));
- assertTrue(parts.containsKey("files"));
- assertEquals("Larry", new String(parts.get("submit-name"), StandardCharsets.UTF_8));
- assertEquals("... contents of file1.txt ...", new String(parts.get("files"), StandardCharsets.UTF_8));
- }
-
- @Test
- void max_length() {
- String part1 = "Larry";
- String part2 = "House 1";
- String data =
- "Content-Type: multipart/form-data; boundary=AaB03x\r\n" +
- "\r\n" +
- "--AaB03x\r\n" +
- "Content-Disposition: form-data; name=\"submit-name\"\r\n" +
- "\r\n" +
- part1 + "\r\n" +
- "--AaB03x\r\n" +
- "Content-Disposition: form-data; name=\"submit-address\"\r\n" +
- "Content-Type: text/plain\r\n" +
- "\r\n" +
- part2 + "\r\n" +
- "--AaB03x--\r\n";
- parse(data, part1.length() + part2.length());
- try {
- parse(data, part1.length() + part2.length() - 1);
- fail("Expected exception");
- } catch (IllegalArgumentException ignored) {
- }
- }
-
- private Map<String, byte[]> parse(String data, long maxLength) {
- ByteArrayInputStream dataStream = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
- HttpRequest request = HttpRequest.createRequest(new MockCurrentContainer(),
- URI.create("http://foo"),
- com.yahoo.jdisc.http.HttpRequest.Method.POST,
- dataStream);
- request.getJDiscRequest().headers().put("Content-Type", "multipart/form-data; boundary=AaB03x");
- return new MultipartParser(maxLength).parse(request);
- }
-
- private static class MockCurrentContainer implements CurrentContainer {
-
- @Override
- public Container newReference(URI uri) { return new MockContainer(); }
-
- }
-
- private static class MockContainer implements Container {
-
- @Override
- public RequestHandler resolveHandler(Request request) { return null; }
-
- @Override
- public <T> T getInstance(Class<T> aClass) { return null; }
-
- @Override
- public ResourceReference refer() { return null; }
-
- @Override
- public void release() { }
-
- @Override
- public long currentTimeMillis() { return 0; }
-
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json
deleted file mode 100644
index 1214d3f73f4..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-clusters.json
+++ /dev/null
@@ -1,143 +0,0 @@
-{
- "clusters": {
- "default": {
- "type": "container",
- "min": {
- "nodes": 2,
- "groups": 1,
- "nodeResources": {
- "vcpu": 1.0,
- "memoryGb": 4.0,
- "diskGb": 20.0,
- "bandwidthGbps": 1.0,
- "diskSpeed": "slow",
- "storageType": "remote",
- "architecture": "any",
- "gpuCount": 0,
- "gpuMemoryGb": 0.0
- },
- "cost": 0.11
- },
- "max": {
- "nodes": 2,
- "groups": 1,
- "nodeResources": {
- "vcpu": 4.0,
- "memoryGb": 16.0,
- "diskGb": 90.0,
- "bandwidthGbps": 1.0,
- "diskSpeed": "slow",
- "storageType": "remote",
- "architecture": "any",
- "gpuCount": 0,
- "gpuMemoryGb": 0.0
- },
- "cost": 0.43
- },
- "groupSize": {
- "to": 3
- },
- "current": {
- "nodes": 2,
- "groups": 1,
- "nodeResources": {
- "vcpu": 2.0,
- "memoryGb": 8.0,
- "diskGb": 50.0,
- "bandwidthGbps": 1.0,
- "diskSpeed": "slow",
- "storageType": "remote",
- "architecture": "any",
- "gpuCount": 0,
- "gpuMemoryGb": 0.0
- },
- "cost": 0.22
- },
- "target": {
- "status": "ideal",
- "description": "Cluster is ideally scaled",
- "resources": {
- "nodes": 2,
- "groups": 1,
- "nodeResources": {
- "vcpu": 3.0,
- "memoryGb": 8.0,
- "diskGb": 50.0,
- "bandwidthGbps": 1.0,
- "diskSpeed": "slow",
- "storageType": "remote",
- "architecture": "any",
- "gpuCount": 0,
- "gpuMemoryGb": 0.0
- },
- "cost": 0.29
- },
- "at" : 123,
- "peak": {
- "cpu": 0.35,
- "memory": 0.65,
- "disk": 1.0
- },
- "ideal": {
- "cpu": 0.2,
- "memory": 0.5,
- "disk": 0.8
- }
- },
- "suggested": {
- "status": "unavailable",
- "description": "",
- "at": 0,
- "peak": {
- "cpu": 0.0,
- "memory": 0.0,
- "disk": 0.0
- },
- "ideal": {
- "cpu": 0.0,
- "memory": 0.0,
- "disk": 0.0
- }
- },
- "scalingEvents": [
- {
- "from": {
- "nodes": 0,
- "groups": 1,
- "nodeResources": {
- "vcpu": 0.0,
- "memoryGb": 0.0,
- "diskGb": 0.0,
- "bandwidthGbps": 0.0,
- "diskSpeed": "fast",
- "storageType": "any",
- "architecture": "any",
- "gpuCount": 0,
- "gpuMemoryGb": 0.0
- },
- "cost": 0.0
- },
- "to": {
- "nodes": 2,
- "groups": 1,
- "nodeResources": {
- "vcpu": 2.0,
- "memoryGb": 8.0,
- "diskGb": 50.0,
- "bandwidthGbps": 1.0,
- "diskSpeed": "slow",
- "storageType": "remote",
- "architecture": "any",
- "gpuCount": 0,
- "gpuMemoryGb": 0.0
- },
- "cost": 0.22
- },
- "at": 1234,
- "completion": 2234
- }
- ],
- "scalingDuration": 360000
- }
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-list.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-list.json
deleted file mode 100644
index 74b9abb1e3b..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-list.json
+++ /dev/null
@@ -1,13 +0,0 @@
-[
- {
- "tenant": "tenant1",
- "application": "application1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1",
- "instances": [
- {
- "instance": "instance1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1"
- }
- ]
- }
-]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-nodes.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-nodes.json
deleted file mode 100644
index 8abb1197d03..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-nodes.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "nodes": [
- {
- "hostname": "host-tenant1.application1.instance1-prod.us-central-1",
- "state": "active",
- "orchestration": "unorchestrated",
- "version": "6.1",
- "flavor": "d-2-8-50",
- "vcpu": 2.0,
- "memoryGb": 8.0,
- "diskGb": 50.0,
- "bandwidthGbps": 1.0,
- "diskSpeed": "slow",
- "storageType": "remote",
- "architecture": "any",
- "gpuCount": 0,
- "gpuMemoryGb": 0.0,
- "clusterId": "default",
- "clusterType": "container",
- "down": false,
- "retired": false,
- "restarting": false,
- "rebooting": false,
- "group": "",
- "index": 0
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-instances.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-instances.json
deleted file mode 100644
index d124d80fe03..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application-without-instances.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "tenant": "tenant1",
- "application": "application1",
- "deployments": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/job/",
- "instances": [ ],
- "pemDeployKeys": [ ],
- "metrics": {
- "queryServiceQuality": 0.0,
- "writeServiceQuality": 0.0
- },
- "activity": { }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json
deleted file mode 100644
index 019fdcb553a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2-with-patches.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "tenant": "tenant2",
- "application": "application2",
- "deployments": "http://localhost:8080/application/v4/tenant/tenant2/application/application2/job/",
- "latestVersion": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "projectId": 1000,
- "majorVersion": 7,
- "instances": [
- {
- "instance": "default",
- "deployments": [ ]
- },
- {
- "instance": "instance1",
- "deploying": {
- "revision": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "changeBlockers": [ ],
- "rotationId": "rotation-id-2",
- "deployments": [ ]
- }
- ],
- "pemDeployKeys": [
- "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n"
- ],
- "metrics": {
- "queryServiceQuality": 0.0,
- "writeServiceQuality": 0.0
- },
- "activity": { }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json
deleted file mode 100644
index 0b673a5852e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/application2.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "tenant": "tenant2",
- "application": "application2",
- "deployments": "http://localhost:8080/application/v4/tenant/tenant2/application/application2/job/",
- "latestVersion": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "projectId": 1000,
- "instances": [
- {
- "instance": "default",
- "deployments": [ ]
- },
- {
- "instance": "instance1",
- "deploying": {
- "revision": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "changeBlockers": [ ],
- "rotationId": "rotation-id-2",
- "deployments": [ ]
- }
- ],
- "pemDeployKeys": [ ],
- "metrics": {
- "queryServiceQuality": 0.0,
- "writeServiceQuality": 0.0
- },
- "activity": { }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json
deleted file mode 100644
index 25fed881dec..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/delete-with-active-deployments.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "error-code": "BAD_REQUEST",
- "message": "Could not delete 'application 'tenant1.application1'': It has active deployments in: dev.us-east-1, prod.us-central-1"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-result.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-result.json
deleted file mode 100644
index 5a037ad8201..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deploy-result.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "message": "Deployed system application hosted-vespa.routing of type proxy in prod.us-central-1 on 6.2",
- "prepareMessages": [ ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-cloud.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-cloud.json
deleted file mode 100644
index b576b32dd0c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-cloud.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "tenant": "scoober",
- "application": "albums",
- "instance": "default",
- "environment": "prod",
- "region": "aws-us-east-1c",
- "availabilityZone": "use1-az2",
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://albums.scoober.aws-us-east-1c.z.vespa-app.cloud/",
- "scope": "zone",
- "routingMethod": "exclusive",
- "legacy": false,
- "authMethod": "mtls"
- }
- ],
- "clusters": "http://localhost:8080/application/v4/tenant/scoober/application/albums/instance/default/environment/prod/region/aws-us-east-1c/clusters",
- "nodes": "http://localhost:8080/zone/v2/prod/aws-us-east-1c/nodes/v2/node/?recursive=true&application=scoober.albums.default",
- "yamasUrl": "http://monitoring-system.test/?environment=prod&region=aws-us-east-1c&application=scoober.albums",
- "version": "7.1.0",
- "revision": "1.0.1-commit1",
- "build": 1,
- "deployTimeEpochMs": 1600000000000,
- "screwdriverId": "1000",
- "applicationVersion": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "status": "complete",
- "quota": 1.304,
- "activity": {},
- "metrics": {
- "queriesPerSecond": 0.0,
- "writesPerSecond": 0.0,
- "documentCount": 0.0,
- "queryLatencyMillis": 0.0,
- "writeLatencyMillis": 0.0
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-job-accepted-2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-job-accepted-2.json
deleted file mode 100644
index 74d5bf454aa..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-job-accepted-2.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "message": "Deployment started in run 1 of dev-us-east-1 for tenant1.application1.myuser. This may take about 15 minutes the first time.",
- "run": 1
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview-2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview-2.json
deleted file mode 100644
index f49b7d9ccae..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview-2.json
+++ /dev/null
@@ -1,1334 +0,0 @@
-{
- "tenant": "tenant",
- "application": "application",
- "projectId": 1001,
- "steps": [
- {
- "type": "instance",
- "dependencies": [ ],
- "declared": true,
- "instance": "default",
- "readyAt": 0,
- "deploying": {
- "application": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "latestVersions": {
- "platform": {
- "platform": "7.1.0",
- "at": 0,
- "upgrade": true,
- "available": [
- {
- "platform": "7.1.0",
- "upgrade": true
- },
- {
- "platform": "6.1.0",
- "upgrade": false
- }
- ],
- "blockers": [
- {
- "days": [
- "Mon",
- "Tue"
- ],
- "hours": [
- 7,
- 8,
- 9,
- 10,
- 11,
- 12,
- 13
- ],
- "zone": "UTC"
- },
- {
- "days": [
- "Sun"
- ],
- "hours": [
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8,
- 9,
- 10,
- 11,
- 12,
- 13,
- 14,
- 15,
- 16,
- 17,
- 18,
- 19,
- 20,
- 21,
- 22,
- 23
- ],
- "zone": "CET"
- }
- ]
- },
- "application": {
- "application": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "at": 1000,
- "upgrade": true,
- "available": [
- {
- "application": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- {
- "application": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- {
- "application": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- ],
- "blockers": [
- {
- "days": [
- "Mon",
- "Tue"
- ],
- "hours": [
- 7,
- 8,
- 9,
- 10,
- 11,
- 12,
- 13
- ],
- "zone": "UTC"
- },
- {
- "days": [
- "Fri",
- "Sat"
- ],
- "hours": [
- 8
- ],
- "zone": "America/Los_Angeles"
- }
- ]
- }
- },
- "delayCause": null
- },
- {
- "type": "test",
- "dependencies": [ ],
- "declared": false,
- "instance": "default",
- "readyAt": 0,
- "jobName": "system-test",
- "url": "https://some.url:43/instance/default/job/system-test",
- "environment": "test",
- "toRun": [ ],
- "runs": [
- {
- "id": 3,
- "url": "https://some.url:43/instance/default/job/system-test/run/3",
- "start": 14403000,
- "end": 14403000,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "us-central-1",
- "build": 3
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 2,
- "url": "https://some.url:43/instance/default/job/system-test/run/2",
- "start": 1000,
- "end": 1000,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "us-central-1",
- "build": 2
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 1,
- "url": "https://some.url:43/instance/default/job/system-test/run/1",
- "start": 0,
- "end": 0,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "us-central-1",
- "build": 1
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": null
- },
- {
- "type": "test",
- "dependencies": [ ],
- "declared": true,
- "instance": "default",
- "readyAt": 15153000,
- "delayedUntil": 15153000,
- "coolingDownUntil": 15153000,
- "jobName": "staging-test",
- "url": "https://some.url:43/instance/default/job/staging-test",
- "environment": "staging",
- "toRun": [
- {
- "dependent": {
- "instance": "default",
- "region": "us-east-3",
- "build": 3
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- }
- ],
- "runs": [
- {
- "id": 5,
- "url": "https://some.url:43/instance/default/job/staging-test/run/5",
- "start": 14503000,
- "end": 14503000,
- "status": "installationFailed",
- "dependent": {
- "instance": "default",
- "region": "us-east-3",
- "build": 3
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "unfinished"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "failed"
- },
- {
- "name": "startStagingSetup",
- "status": "unfinished"
- },
- {
- "name": "endStagingSetup",
- "status": "unfinished"
- },
- {
- "name": "deployReal",
- "status": "unfinished"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "startTests",
- "status": "unfinished"
- },
- {
- "name": "endTests",
- "status": "unfinished"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 4,
- "url": "https://some.url:43/instance/default/job/staging-test/run/4",
- "start": 14403000,
- "end": 14403000,
- "status": "installationFailed",
- "dependent": {
- "instance": "default",
- "region": "us-east-3",
- "build": 3
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "unfinished"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "failed"
- },
- {
- "name": "startStagingSetup",
- "status": "unfinished"
- },
- {
- "name": "endStagingSetup",
- "status": "unfinished"
- },
- {
- "name": "deployReal",
- "status": "unfinished"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "startTests",
- "status": "unfinished"
- },
- {
- "name": "endTests",
- "status": "unfinished"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 3,
- "url": "https://some.url:43/instance/default/job/staging-test/run/3",
- "start": 14403000,
- "end": 14403000,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "us-central-1",
- "build": 3
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "succeeded"
- },
- {
- "name": "startStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "endStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 2,
- "url": "https://some.url:43/instance/default/job/staging-test/run/2",
- "start": 1000,
- "end": 1000,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "us-central-1",
- "build": 2
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "succeeded"
- },
- {
- "name": "startStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "endStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 1,
- "url": "https://some.url:43/instance/default/job/staging-test/run/1",
- "start": 0,
- "end": 0,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "us-central-1",
- "build": 1
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "succeeded"
- },
- {
- "name": "startStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "endStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": "coolingDown"
- },
- {
- "type": "deployment",
- "dependencies": [
- 0,
- 2
- ],
- "declared": true,
- "instance": "default",
- "readyAt": 14403000,
- "delayedUntil": 14403000,
- "jobName": "production-us-central-1",
- "url": "https://some.url:43/instance/default/job/production-us-central-1",
- "environment": "prod",
- "region": "prod.us-central-1",
- "currentPlatform": "6.1.0",
- "currentApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "toRun": [ ],
- "runs": [
- {
- "id": 3,
- "url": "https://some.url:43/instance/default/job/production-us-central-1/run/3",
- "start": 14403000,
- "status": "running",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "report",
- "status": "unfinished"
- }
- ]
- },
- {
- "id": 2,
- "url": "https://some.url:43/instance/default/job/production-us-central-1/run/2",
- "start": 1000,
- "end": 1000,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 1,
- "url": "https://some.url:43/instance/default/job/production-us-central-1/run/1",
- "start": 0,
- "end": 0,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": null
- },
- {
- "type": "test",
- "dependencies": [
- 3
- ],
- "declared": true,
- "instance": "default",
- "jobName": "test-us-central-1",
- "url": "https://some.url:43/instance/default/job/test-us-central-1",
- "environment": "prod",
- "region": "prod.us-central-1",
- "toRun": [
- {
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- }
- ],
- "runs": [
- {
- "id": 2,
- "url": "https://some.url:43/instance/default/job/test-us-central-1/run/2",
- "start": 1000,
- "end": 1000,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 1,
- "url": "https://some.url:43/instance/default/job/test-us-central-1/run/1",
- "start": 0,
- "end": 0,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": "notReady"
- },
- {
- "type": "deployment",
- "dependencies": [
- 4
- ],
- "declared": true,
- "instance": "default",
- "jobName": "production-us-west-1",
- "url": "https://some.url:43/instance/default/job/production-us-west-1",
- "environment": "prod",
- "region": "prod.us-west-1",
- "currentPlatform": "6.1.0",
- "currentApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "toRun": [
- {
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- }
- ],
- "runs": [
- {
- "id": 2,
- "url": "https://some.url:43/instance/default/job/production-us-west-1/run/2",
- "start": 1000,
- "end": 14402000,
- "status": "installationFailed",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "failed"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 1,
- "url": "https://some.url:43/instance/default/job/production-us-west-1/run/1",
- "start": 0,
- "end": 0,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": "notReady"
- },
- {
- "type": "deployment",
- "dependencies": [
- 4
- ],
- "declared": true,
- "instance": "default",
- "jobName": "production-us-east-3",
- "url": "https://some.url:43/instance/default/job/production-us-east-3",
- "environment": "prod",
- "region": "prod.us-east-3",
- "currentPlatform": "6.1.0",
- "currentApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "toRun": [
- {
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- }
- ],
- "runs": [
- {
- "id": 2,
- "url": "https://some.url:43/instance/default/job/production-us-east-3/run/2",
- "start": 1000,
- "end": 1000,
- "status": "deploymentFailed",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "failed"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 1,
- "url": "https://some.url:43/instance/default/job/production-us-east-3/run/1",
- "start": 0,
- "end": 0,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": "notReady"
- }
- ],
- "builds": [
- {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "deployable": true,
- "submittedAt": 14403000
- },
- {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "deployable": true,
- "submittedAt": 1000
- },
- {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "deployable": true,
- "submittedAt": 0
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview.json
deleted file mode 100644
index 617fb48a281..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-overview.json
+++ /dev/null
@@ -1,763 +0,0 @@
-{
- "tenant": "tenant1",
- "application": "application1",
- "projectId": 123,
- "steps": [
- {
- "type": "instance",
- "dependencies": [ ],
- "declared": true,
- "instance": "instance1",
- "readyAt": 0,
- "deploying": {
- "application": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "latestVersions": {
- "platform": {
- "platform": "6.1.0",
- "at": 1600000000000,
- "upgrade": true,
- "available": [
- {
- "platform": "6.1.0",
- "upgrade": true
- }
- ],
- "blockers": [ ]
- },
- "application": {
- "application": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "at": 1000,
- "upgrade": false,
- "available": [
- {
- "application": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- {
- "application": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- ],
- "blockers": [ ]
- }
- },
- "delayCause": null
- },
- {
- "type": "test",
- "dependencies": [ ],
- "declared": false,
- "instance": "instance1",
- "readyAt": 0,
- "delayedUntil": 0,
- "jobName": "system-test",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test",
- "environment": "test",
- "toRun": [ ],
- "runs": [
- {
- "id": 2,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test/run/2",
- "start": 1600000000000,
- "status": "running",
- "dependent": {
- "instance": "instance1",
- "region": "us-central-1",
- "build": 4
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "unfinished"
- },
- {
- "name": "installTester",
- "status": "unfinished"
- },
- {
- "name": "deployReal",
- "status": "unfinished"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "startTests",
- "status": "unfinished"
- },
- {
- "name": "endTests",
- "status": "unfinished"
- },
- {
- "name": "copyVespaLogs",
- "status": "unfinished"
- },
- {
- "name": "deactivateReal",
- "status": "unfinished"
- },
- {
- "name": "deactivateTester",
- "status": "unfinished"
- },
- {
- "name": "report",
- "status": "unfinished"
- }
- ]
- },
- {
- "id": 1,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test/run/1",
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "dependent": {
- "instance": "instance1",
- "region": "us-central-1",
- "build": 1
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": null
- },
- {
- "type": "test",
- "dependencies": [ ],
- "declared": false,
- "instance": "instance1",
- "readyAt": 0,
- "delayedUntil": 0,
- "jobName": "staging-test",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/staging-test",
- "environment": "staging",
- "toRun": [ ],
- "runs": [
- {
- "id": 2,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/staging-test/run/2",
- "start": 1600000000000,
- "status": "running",
- "dependent": {
- "instance": "instance1",
- "region": "us-central-1",
- "build": 4
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "unfinished"
- },
- {
- "name": "installTester",
- "status": "unfinished"
- },
- {
- "name": "deployInitialReal",
- "status": "unfinished"
- },
- {
- "name": "installInitialReal",
- "status": "unfinished"
- },
- {
- "name": "startStagingSetup",
- "status": "unfinished"
- },
- {
- "name": "endStagingSetup",
- "status": "unfinished"
- },
- {
- "name": "deployReal",
- "status": "unfinished"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "startTests",
- "status": "unfinished"
- },
- {
- "name": "endTests",
- "status": "unfinished"
- },
- {
- "name": "copyVespaLogs",
- "status": "unfinished"
- },
- {
- "name": "deactivateReal",
- "status": "unfinished"
- },
- {
- "name": "deactivateTester",
- "status": "unfinished"
- },
- {
- "name": "report",
- "status": "unfinished"
- }
- ]
- },
- {
- "id": 1,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/staging-test/run/1",
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "dependent": {
- "instance": "instance1",
- "region": "us-central-1",
- "build": 1
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "succeeded"
- },
- {
- "name": "startStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "endStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": null
- },
- {
- "type": "deployment",
- "dependencies": [
- 0
- ],
- "declared": true,
- "instance": "instance1",
- "jobName": "production-us-central-1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-central-1",
- "environment": "prod",
- "region": "prod.us-central-1",
- "toRun": [
- {
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- }
- ],
- "runs": [
- {
- "id": 1,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-central-1/run/1",
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": "unverified"
- },
- {
- "type": "deployment",
- "dependencies": [
- 3
- ],
- "declared": true,
- "instance": "instance1",
- "jobName": "production-us-west-1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-west-1",
- "environment": "prod",
- "region": "prod.us-west-1",
- "toRun": [
- {
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- }
- ],
- "runs": [
- {
- "id": 1,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-west-1/run/1",
- "start": 1600000000000,
- "status": "running",
- "reason": "triggered by user.myuser",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "unfinished"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "report",
- "status": "unfinished"
- }
- ]
- }
- ],
- "delayCause": "notReady"
- },
- {
- "type": "deployment",
- "dependencies": [
- 3
- ],
- "declared": true,
- "instance": "instance1",
- "jobName": "production-us-east-3",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-east-3",
- "environment": "prod",
- "region": "prod.us-east-3",
- "toRun": [
- {
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- }
- ],
- "runs": [
- {
- "id": 2,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-east-3/run/2",
- "start": 1600000000000,
- "status": "running",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "unfinished"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "report",
- "status": "unfinished"
- }
- ]
- },
- {
- "id": 1,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/production-us-east-3/run/1",
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": "notReady"
- },
- {
- "type": "instance",
- "dependencies": [
- 4,
- 5
- ],
- "declared": true,
- "instance": "instance2",
- "deploying": {
- "application": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "latestVersions": {
- "platform": {
- "platform": "6.1.0",
- "at": 1600000000000,
- "upgrade": true,
- "available": [
- {
- "platform": "6.1.0",
- "upgrade": true
- }
- ],
- "blockers": [ ]
- },
- "application": {
- "application": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "at": 1000,
- "upgrade": false,
- "available": [
- {
- "application": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- {
- "application": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- ],
- "blockers": [ ]
- }
- },
- "delayCause": "notReady"
- },
- {
- "type": "deployment",
- "dependencies": [
- 6
- ],
- "declared": true,
- "instance": "instance2",
- "jobName": "production-us-central-1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance2/job/production-us-central-1",
- "environment": "prod",
- "region": "prod.us-central-1",
- "toRun": [
- {
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- }
- ],
- "runs": [ ],
- "delayCause": "unverified"
- },
- {
- "type": "deployment",
- "dependencies": [
- 7
- ],
- "declared": true,
- "instance": "instance2",
- "jobName": "production-us-west-1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance2/job/production-us-west-1",
- "environment": "prod",
- "region": "prod.us-west-1",
- "toRun": [
- {
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- }
- ],
- "runs": [ ],
- "delayCause": "notReady"
- },
- {
- "type": "deployment",
- "dependencies": [
- 7
- ],
- "declared": true,
- "instance": "instance2",
- "jobName": "production-us-east-3",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance2/job/production-us-east-3",
- "environment": "prod",
- "region": "prod.us-east-3",
- "toRun": [
- {
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- }
- ],
- "runs": [ ],
- "delayCause": "notReady"
- }
- ],
- "builds": [
- {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "description": "my best commit yet",
- "risk": 9001,
- "deployable": true,
- "submittedAt": 1600000000000
- },
- {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "description": "my best commit yet",
- "risk": 9001,
- "deployable": false,
- "submittedAt": 1600000000000
- },
- {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "description": "my best commit yet",
- "risk": 9001,
- "deployable": false,
- "submittedAt": 1600000000000
- },
- {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "description": "my best commit yet",
- "risk": 9001,
- "deployable": true,
- "submittedAt": 1600000000000
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-with-routing-policy.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-with-routing-policy.json
deleted file mode 100644
index 9694df32e9f..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-with-routing-policy.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "prod",
- "region": "us-west-1",
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://instance1.application1.tenant1.us-west-1.vespa.oath.cloud/",
- "scope": "zone",
- "routingMethod": "exclusive",
- "legacy": false,
- "authMethod": "mtls"
- }
- ],
- "clusters": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/clusters",
- "nodes": "http://localhost:8080/zone/v2/prod/us-west-1/nodes/v2/node/?recursive=true&application=tenant1.application1.instance1",
- "yamasUrl": "http://monitoring-system.test/?environment=prod&region=us-west-1&application=tenant1.application1.instance1",
- "version": "6.1.0",
- "revision": "1.0.1-commit1",
- "build": 1,
- "deployTimeEpochMs": 1600000000000,
- "screwdriverId": "1000",
- "applicationVersion": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "status": "complete",
- "quota": 1.304,
- "activity": { },
- "metrics": {
- "queriesPerSecond": 0.0,
- "writesPerSecond": 0.0,
- "documentCount": 0.0,
- "queryLatencyMillis": 0.0,
- "writeLatencyMillis": 0.0
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-without-shared-endpoints.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-without-shared-endpoints.json
deleted file mode 100644
index 9694df32e9f..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment-without-shared-endpoints.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "prod",
- "region": "us-west-1",
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://instance1.application1.tenant1.us-west-1.vespa.oath.cloud/",
- "scope": "zone",
- "routingMethod": "exclusive",
- "legacy": false,
- "authMethod": "mtls"
- }
- ],
- "clusters": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1/clusters",
- "nodes": "http://localhost:8080/zone/v2/prod/us-west-1/nodes/v2/node/?recursive=true&application=tenant1.application1.instance1",
- "yamasUrl": "http://monitoring-system.test/?environment=prod&region=us-west-1&application=tenant1.application1.instance1",
- "version": "6.1.0",
- "revision": "1.0.1-commit1",
- "build": 1,
- "deployTimeEpochMs": 1600000000000,
- "screwdriverId": "1000",
- "applicationVersion": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "status": "complete",
- "quota": 1.304,
- "activity": { },
- "metrics": {
- "queriesPerSecond": 0.0,
- "writesPerSecond": 0.0,
- "documentCount": 0.0,
- "queryLatencyMillis": 0.0,
- "writeLatencyMillis": 0.0
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json
deleted file mode 100644
index b7517f1fcf6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json
+++ /dev/null
@@ -1,77 +0,0 @@
-{
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "prod",
- "region": "us-central-1",
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://instance1.application1.tenant1.us-central-1.vespa.oath.cloud/",
- "scope": "zone",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- },
- {
- "id": "default",
- "cluster": "foo",
- "tls": true,
- "url": "https://instance1.application1.tenant1.global.vespa.oath.cloud/",
- "scope": "global",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- },
- {
- "id": "a0",
- "cluster": "foo",
- "tls": true,
- "url": "https://a0.application1.tenant1.a.vespa.oath.cloud/",
- "scope": "application",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- }
- ],
- "clusters": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/clusters",
- "nodes": "http://localhost:8080/zone/v2/prod/us-central-1/nodes/v2/node/?recursive=true&application=tenant1.application1.instance1",
- "yamasUrl": "http://monitoring-system.test/?environment=prod&region=us-central-1&application=tenant1.application1.instance1",
- "version": "6.1.0",
- "revision": "1.0.1-commit1",
- "build": 1,
- "deployTimeEpochMs": 1600000000000,
- "screwdriverId": "123",
- "endpointStatus": [
- {
- "endpointId": "default",
- "rotationId": "rotation-id-1",
- "clusterId": "foo",
- "status": "UNKNOWN",
- "lastUpdated": 0
- }
- ],
- "applicationVersion": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "status": "complete",
- "quota": 1.304,
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- },
- "metrics": {
- "queriesPerSecond": 1.0,
- "writesPerSecond": 2.0,
- "documentCount": 3.0,
- "queryLatencyMillis": 4.0,
- "writeLatencyMillis": 5.0,
- "lastUpdated": 123123
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-aws-us-east-2a-runs.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-aws-us-east-2a-runs.json
deleted file mode 100644
index 313737146fb..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-aws-us-east-2a-runs.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "runs": [
- {
- "id": 1,
- "url": "https://some.url:43/root/run/1",
- "start": 14503000,
- "end": 14503000,
- "status": "success",
- "versions": {
- "targetPlatform": "7.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-overview.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-overview.json
deleted file mode 100644
index ea0e6a4b442..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-overview.json
+++ /dev/null
@@ -1,69 +0,0 @@
-{
- "deployment": [
- {
- "jobName": "dev-us-east-1",
- "runs": [
- {
- "id": 2,
- "url": "https://some.url:43/root/run/2",
- "start": 0,
- "status": "running",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 2,
- "compileVersion": "6.1.0"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "copyVespaLogs",
- "status": "unfinished"
- }
- ]
- },
- {
- "id": 1,
- "url": "https://some.url:43/root/run/1",
- "start": 0,
- "end": 0,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- }
- ]
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1-log-first-part.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1-log-first-part.json
deleted file mode 100644
index 3190dee06fa..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1-log-first-part.json
+++ /dev/null
@@ -1,87 +0,0 @@
-{
- "active": true,
- "status": "running",
- "log": {
- "deployReal": [
- {
- "at": 0,
- "type": "info",
- "message": "Deploying platform version 6.1 and application dev build 1 for dev-us-east-1 of default ..."
- },
- {
- "at": 0,
- "type": "info",
- "message": "Using CA signed certificate version 0"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Deployment successful."
- },
- {
- "at": 0,
- "type": "info",
- "message": "foo"
- }
- ],
- "installReal": [
- {
- "at": 0,
- "type": "info",
- "message": "######## Details for all nodes ########"
- },
- {
- "at": 0,
- "type": "info",
- "message": "host-tenant.application.default-dev.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- }
- ],
- "copyVespaLogs": [
- {
- "at": 1554970337935,
- "type": "info",
- "message": "17491290-v6-1.ostk.bm2.prod.ne1.yahoo.com\tcontainer\tstdout\nERROR: Bundle canary-application [71] Unable to get module class path. (java.lang.NullPointerException)"
- }
- ]
- },
- "lastId": 8,
- "steps": {
- "deployReal": {
- "status": "succeeded",
- "startMillis": 0
- },
- "installReal": {
- "status": "unfinished",
- "startMillis": 0,
- "convergence": {
- "nodes": 1,
- "down": 0,
- "needPlatformUpgrade": 0,
- "upgrading": 0,
- "needReboot": 0,
- "rebooting": 0,
- "needRestart": 0,
- "restarting": 0,
- "upgradingOs": 0,
- "upgradingFirmware": 0,
- "services": 1,
- "needNewConfig": 1,
- "retiring": 0
- }
- },
- "copyVespaLogs": {
- "status": "unfinished"
- }
- },
- "vespaLogsActive": true
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1-log-second-part.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1-log-second-part.json
deleted file mode 100644
index cee7e0f4c92..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/dev-us-east-1-log-second-part.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "active": false,
- "status": "success",
- "log": {
- "installReal": [
- {
- "at": 0,
- "type": "info",
- "message": "Found endpoints:"
- },
- {
- "at": 0,
- "type": "info",
- "message": "- dev.us-east-1"
- },
- {
- "at": 0,
- "type": "info",
- "message": " |-- https://application.tenant.us-east-1.dev.vespa.oath.cloud/ (cluster 'default')"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Installation succeeded!"
- }
- ]
- },
- "lastId": 12,
- "steps": {
- "deployReal": {
- "status": "succeeded",
- "startMillis": 0
- },
- "installReal": {
- "status": "succeeded",
- "startMillis": 0
- },
- "copyVespaLogs": {
- "status": "succeeded",
- "startMillis": 0
- }
- },
- "vespaLogsActive": false
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-delete.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-delete.json
deleted file mode 100644
index 4b97410b21c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-delete.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "message": "Successfully set tenant1.application1.instance1 in prod.us-west-1 in service"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-get.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-get.json
deleted file mode 100644
index de2266fd197..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-get.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "globalrotationoverride": [
- "foo.instance1.application1.tenant1.us-west-1.prod",
- {
- "status": "in",
- "reason": "",
- "agent": "unknown",
- "timestamp": 1497618757
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-put.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-put.json
deleted file mode 100644
index e7f5c2407bd..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation-put.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "message": "Successfully set tenant1.application1.instance1 in prod.us-west-1 out of service"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation.json
deleted file mode 100644
index ea8c63cffb6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/global-rotation.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "bcpStatus": {
- "rotationStatus": "UNKNOWN"
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference-2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference-2.json
deleted file mode 100644
index 0c9dc2ca1e7..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference-2.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "tenant": "tenant2",
- "application": "application2",
- "instance": "default",
- "url": "http://localhost:8080/application/v4/tenant/tenant2/application/application2/instance/default"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference-default.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference-default.json
deleted file mode 100644
index cf964b0a1ae..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference-default.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "tenant": "tenant1",
- "application": "application1",
- "instance": "default",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/default"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference.json
deleted file mode 100644
index e25f992ab25..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-reference.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-with-routing-policy.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-with-routing-policy.json
deleted file mode 100644
index 3d722168d52..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance-with-routing-policy.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "deployments": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "projectId": 1000,
- "changeBlockers": [ ],
- "instances": [
- {
- "environment": "prod",
- "region": "us-west-1",
- "instance": "instance1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1"
- }
- ],
- "pemDeployKeys": [ ],
- "metrics": {
- "queryServiceQuality": 0.0,
- "writeServiceQuality": 0.0
- },
- "activity": { }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json
deleted file mode 100644
index b94d8fda83a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance.json
+++ /dev/null
@@ -1,88 +0,0 @@
-{
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "deployments": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "projectId": 123,
- "deploying": {
- "revision": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "changeBlockers": [
- {
- "versions": true,
- "revisions": false,
- "timeZone": "UTC",
- "days": [
- 1,
- 2,
- 3,
- 4,
- 5
- ],
- "hours": [
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8
- ]
- }
- ],
- "rotationId": "rotation-id-1",
- "instances": [
- {
- "environment": "dev",
- "region": "us-east-1",
- "instance": "instance1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/dev/region/us-east-1"
- },
- {
- "bcpStatus": {
- "rotationStatus": "UNKNOWN"
- },
- "endpointStatus": [
- {
- "endpointId": "default",
- "rotationId": "rotation-id-1",
- "clusterId": "foo",
- "status": "UNKNOWN",
- "lastUpdated": 0
- }
- ],
- "environment": "prod",
- "region": "us-central-1",
- "instance": "instance1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1"
- },
- {
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "environment": "prod",
- "region": "us-west-1"
- }
- ],
- "pemDeployKeys": [ ],
- "metrics": {
- "queryServiceQuality": 0.5,
- "writeServiceQuality": 0.7
- },
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json
deleted file mode 100644
index 9f54a16acfd..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json
+++ /dev/null
@@ -1,189 +0,0 @@
-{
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "deployments": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "projectId": 123,
- "deploying": {
- "revision": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "changeBlockers": [
- {
- "versions": true,
- "revisions": false,
- "timeZone": "UTC",
- "days": [
- 1,
- 2,
- 3,
- 4,
- 5
- ],
- "hours": [
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8
- ]
- }
- ],
- "rotationId": "rotation-id-1",
- "instances": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "dev",
- "region": "us-east-1",
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://instance1.application1.tenant1.us-east-1.dev.vespa.oath.cloud/",
- "scope": "zone",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- }
- ],
- "clusters": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/dev/region/us-east-1/clusters",
- "nodes": "http://localhost:8080/zone/v2/dev/us-east-1/nodes/v2/node/?recursive=true&application=tenant1.application1.instance1",
- "yamasUrl": "http://monitoring-system.test/?environment=dev&region=us-east-1&application=tenant1.application1.instance1",
- "version": "6.1.0",
- "revision": "1.0.1",
- "build": 1,
- "deployTimeEpochMs": 1600000000000,
- "screwdriverId": "123",
- "status": "complete",
- "quota": 1.304,
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- },
- "metrics": {
- "queriesPerSecond": 1.0,
- "writesPerSecond": 2.0,
- "documentCount": 3.0,
- "queryLatencyMillis": 4.0,
- "writeLatencyMillis": 5.0,
- "lastUpdated": 123123
- }
- },
- {
- "bcpStatus": {
- "rotationStatus": "UNKNOWN"
- },
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "prod",
- "region": "us-central-1",
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://instance1.application1.tenant1.us-central-1.vespa.oath.cloud/",
- "scope": "zone",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- },
- {
- "id": "default",
- "cluster": "foo",
- "tls": true,
- "url": "https://instance1.application1.tenant1.global.vespa.oath.cloud/",
- "scope": "global",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- },
- {
- "id": "a0",
- "cluster": "foo",
- "tls": true,
- "url": "https://a0.application1.tenant1.a.vespa.oath.cloud/",
- "scope": "application",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- }
- ],
- "clusters": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/clusters",
- "nodes": "http://localhost:8080/zone/v2/prod/us-central-1/nodes/v2/node/?recursive=true&application=tenant1.application1.instance1",
- "yamasUrl": "http://monitoring-system.test/?environment=prod&region=us-central-1&application=tenant1.application1.instance1",
- "version": "6.1.0",
- "revision": "1.0.1-commit1",
- "build": 1,
- "deployTimeEpochMs": 1600000000000,
- "screwdriverId": "123",
- "endpointStatus": [
- {
- "endpointId": "default",
- "rotationId": "rotation-id-1",
- "clusterId": "foo",
- "status": "UNKNOWN",
- "lastUpdated": 0
- }
- ],
- "applicationVersion": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "status": "complete",
- "quota": 1.304,
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- },
- "metrics": {
- "queriesPerSecond": 1.0,
- "writesPerSecond": 2.0,
- "documentCount": 3.0,
- "queryLatencyMillis": 4.0,
- "writeLatencyMillis": 5.0,
- "lastUpdated": 123123
- }
- },
- {
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "environment": "prod",
- "region": "us-west-1"
- }
- ],
- "pemDeployKeys": [ ],
- "metrics": {
- "queryServiceQuality": 0.5,
- "writeServiceQuality": 0.7
- },
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- },
- "ownershipIssueId": "321",
- "owner": "owner-account-id",
- "deploymentIssueId": "123"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs-direct-deployment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs-direct-deployment.json
deleted file mode 100644
index ed916b1406c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs-direct-deployment.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "deployment": [ ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json
deleted file mode 100644
index 2477e8df56e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/jobs.json
+++ /dev/null
@@ -1,71 +0,0 @@
-{
- "deployment": [
- {
- "jobName": "dev-us-east-1",
- "runs": [
- {
- "id": 2,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/run/2",
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "reason": "triggered by user.myuser",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 1,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/run/1",
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- }
- ]
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-applicationPackage.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-applicationPackage.json
deleted file mode 100644
index 82725213aaa..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-applicationPackage.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "notifications": [
- {
- "at": 1600000000000,
- "level": "warning",
- "type": "applicationPackage",
- "tenant": "tenant1",
- "application": "app1"
- },
- {
- "at": 1600000000000,
- "level": "warning",
- "type": "applicationPackage",
- "tenant": "tenant2",
- "application": "app1"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1-app2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1-app2.json
deleted file mode 100644
index 6206e3b277a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1-app2.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "notifications": [
- {
- "at": 1600000000000,
- "level": "error",
- "type": "deployment",
- "title": "[System test #12](https://console.tld/tenant/tenant1/application/app2/prod/instance/instance1/job/system-test/run/12) for application **app2.instance1** has failed",
- "messages": [
- "Failed to deploy: Node allocation failure"
- ],
- "application": "app2",
- "instance": "instance1",
- "jobName": "system-test",
- "runNumber": 12
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1.json
deleted file mode 100644
index 78deea65008..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/notifications-tenant1.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "notifications": [
- {
- "at": 1600000000000,
- "level": "warning",
- "type": "applicationPackage",
- "title": "Application package for [app1](https://console.tld/tenant/tenant1/application/app1/prod/instance) has a warning",
- "messages": [
- "Something something deprecated..."
- ],
- "application": "app1"
- },
- {
- "at": 1600000000000,
- "level": "error",
- "type": "deployment",
- "title": "[System test #12](https://console.tld/tenant/tenant1/application/app2/prod/instance/instance1/job/system-test/run/12) for application **app2.instance1** has failed",
- "messages": [
- "Failed to deploy: Node allocation failure"
- ],
- "application": "app2",
- "instance": "instance1",
- "jobName": "system-test",
- "runNumber": 12
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview-enclave.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview-enclave.json
deleted file mode 100644
index 1d2cd8eaabb..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview-enclave.json
+++ /dev/null
@@ -1,305 +0,0 @@
-{
- "tenant": "tenant",
- "application": "application",
- "projectId": 1000,
- "steps": [
- {
- "type": "instance",
- "dependencies": [ ],
- "declared": true,
- "instance": "default",
- "readyAt": 0,
- "deploying": { },
- "latestVersions": {
- "platform": {
- "platform": "6.1.0",
- "at": 1600000000000,
- "upgrade": false,
- "available": [
- {
- "platform": "6.1.0",
- "upgrade": false
- }
- ],
- "blockers": [ ]
- },
- "application": {
- "application": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "at": 1000,
- "upgrade": false,
- "available": [
- {
- "application": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- }
- ],
- "blockers": [ ]
- }
- },
- "delayCause": null
- },
- {
- "type": "test",
- "dependencies": [ ],
- "declared": true,
- "instance": "default",
- "readyAt": 0,
- "jobName": "staging-test",
- "url": "https://some.url:43/instance/default/job/staging-test",
- "environment": "staging",
- "toRun": [ ],
- "enclave": {
- "cloudAccount": "aws:123456789012"
- },
- "runs": [
- {
- "id": 1,
- "url": "https://some.url:43/instance/default/job/staging-test/run/1",
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "aws-us-east-1c",
- "build": 1
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "enclave": {
- "cloudAccount": "aws:123456789012"
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "succeeded"
- },
- {
- "name": "startStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "endStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": null
- },
- {
- "type": "test",
- "dependencies": [ ],
- "declared": true,
- "instance": "default",
- "readyAt": 0,
- "jobName": "system-test",
- "url": "https://some.url:43/instance/default/job/system-test",
- "environment": "test",
- "toRun": [ ],
- "enclave": {
- "cloudAccount": "aws:123456789012"
- },
- "runs": [
- {
- "id": 1,
- "url": "https://some.url:43/instance/default/job/system-test/run/1",
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "aws-us-east-1c",
- "build": 1
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "enclave": {
- "cloudAccount": "aws:123456789012"
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": null
- },
- {
- "type": "deployment",
- "dependencies": [
- 0,
- 1,
- 2
- ],
- "declared": true,
- "instance": "default",
- "readyAt": 1600000000000,
- "jobName": "production-aws-us-east-1c",
- "url": "https://some.url:43/instance/default/job/production-aws-us-east-1c",
- "environment": "prod",
- "region": "prod.aws-us-east-1c",
- "currentPlatform": "6.1.0",
- "currentApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "toRun": [ ],
- "enclave": {
- "cloudAccount": "aws:123456789012"
- },
- "runs": [
- {
- "id": 1,
- "url": "https://some.url:43/instance/default/job/production-aws-us-east-1c/run/1",
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "enclave": {
- "cloudAccount": "aws:123456789012"
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ],
- "delayCause": null
- }
- ],
- "builds": [
- {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "deployable": true,
- "submittedAt": 1600000000000
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview-user-instance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview-user-instance.json
deleted file mode 100644
index 6522d91800c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview-user-instance.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "deployment": [
- {
- "jobName": "dev-aws-us-east-2a",
- "runs": [
- {
- "id": 1,
- "url": "https://some.url:43/root//run/1",
- "start": 14503000,
- "end": 14503000,
- "status": "success",
- "versions": {
- "targetPlatform": "7.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0"
- }
- },
- "steps": [
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- }
- ]
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview.json
deleted file mode 100644
index ed916b1406c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/overview.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "deployment": [ ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/proton-metrics.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/proton-metrics.json
deleted file mode 100644
index 3fba9b3c91c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/proton-metrics.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "metrics": [
- {
- "clusterId": "content/doc/",
- "metrics": {
- "resourceMemoryUsageAverage": 0.103482,
- "documentsReadyCount": 11430.0,
- "documentDiskUsage": 44021.0,
- "resourceDiskUsageAverage": 0.0168421,
- "documentsTotalCount": 11430.0,
- "documentsActiveCount": 11430.0
- }
- },
- {
- "clusterId": "content/music/",
- "metrics": {
- "resourceMemoryUsageAverage": 0.00912,
- "documentsReadyCount": 32000.0,
- "documentDiskUsage": 90113.0,
- "resourceDiskUsageAverage": 0.23912,
- "documentsTotalCount": 32210.0,
- "documentsActiveCount": 32210.0
- }
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json
deleted file mode 100644
index 4f2ff7560c9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json
+++ /dev/null
@@ -1,228 +0,0 @@
-[
- {
- "tenant": "tenant1",
- "type": "ATHENS",
- "athensDomain": "domain1",
- "property": "property1",
- "applications": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "deployments": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "projectId": 123,
- "deploying": {
- "revision": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "changeBlockers": [
- {
- "versions": true,
- "revisions": false,
- "timeZone": "UTC",
- "days": [
- 1,
- 2,
- 3,
- 4,
- 5
- ],
- "hours": [
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8
- ]
- }
- ],
- "rotationId": "rotation-id-1",
- "instances": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "dev",
- "region": "us-east-1",
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://instance1.application1.tenant1.us-east-1.dev.vespa.oath.cloud/",
- "scope": "zone",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- }
- ],
- "clusters": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/dev/region/us-east-1/clusters",
- "nodes": "http://localhost:8080/zone/v2/dev/us-east-1/nodes/v2/node/?recursive=true&application=tenant1.application1.instance1",
- "yamasUrl": "http://monitoring-system.test/?environment=dev&region=us-east-1&application=tenant1.application1.instance1",
- "version": "6.1.0",
- "revision": "1.0.1",
- "build": 1,
- "deployTimeEpochMs": 1600000000000,
- "screwdriverId": "123",
- "status": "complete",
- "quota": 1.304,
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- },
- "metrics": {
- "queriesPerSecond": 1.0,
- "writesPerSecond": 2.0,
- "documentCount": 3.0,
- "queryLatencyMillis": 4.0,
- "writeLatencyMillis": 5.0,
- "lastUpdated": 123123
- }
- },
- {
- "bcpStatus": {
- "rotationStatus": "UNKNOWN"
- },
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "prod",
- "region": "us-central-1",
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://instance1.application1.tenant1.us-central-1.vespa.oath.cloud/",
- "scope": "zone",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- },
- {
- "id": "default",
- "cluster": "foo",
- "tls": true,
- "url": "https://instance1.application1.tenant1.global.vespa.oath.cloud/",
- "scope": "global",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- },
- {
- "id": "a0",
- "cluster": "foo",
- "tls": true,
- "url": "https://a0.application1.tenant1.a.vespa.oath.cloud/",
- "scope": "application",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- }
- ],
- "clusters": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/clusters",
- "nodes": "http://localhost:8080/zone/v2/prod/us-central-1/nodes/v2/node/?recursive=true&application=tenant1.application1.instance1",
- "yamasUrl": "http://monitoring-system.test/?environment=prod&region=us-central-1&application=tenant1.application1.instance1",
- "version": "6.1.0",
- "revision": "1.0.1-commit1",
- "build": 1,
- "deployTimeEpochMs": 1600000000000,
- "screwdriverId": "123",
- "endpointStatus": [
- {
- "endpointId": "default",
- "rotationId": "rotation-id-1",
- "clusterId": "foo",
- "status": "UNKNOWN",
- "lastUpdated": 0
- }
- ],
- "applicationVersion": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "status": "complete",
- "quota": 1.304,
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- },
- "metrics": {
- "queriesPerSecond": 1.0,
- "writesPerSecond": 2.0,
- "documentCount": 3.0,
- "queryLatencyMillis": 4.0,
- "writeLatencyMillis": 5.0,
- "lastUpdated": 123123
- }
- },
- {
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "environment": "prod",
- "region": "us-west-1"
- }
- ],
- "pemDeployKeys": [ ],
- "metrics": {
- "queryServiceQuality": 0.5,
- "writeServiceQuality": 0.7
- },
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- },
- "ownershipIssueId": "321",
- "owner": "owner-account-id",
- "deploymentIssueId": "123"
- }
- ],
- "metaData": {
- "createdAtMillis": 1600000000000,
- "lastDeploymentToDevMillis": 1600000000000,
- "lastSubmissionToProdMillis": 1000
- }
- },
- {
- "tenant": "tenant2",
- "type": "ATHENS",
- "athensDomain": "domain2",
- "property": "property2",
- "propertyId": "1234",
- "propertyUrl": "www.properties.tld/1234",
- "contactsUrl": "www.contacts.tld/1234",
- "issueCreationUrl": "www.issues.tld/1234",
- "contacts": [
- [
- "alice"
- ],
- [
- "bob"
- ]
- ],
- "applications": [ ],
- "metaData": {
- "createdAtMillis": 1600000000000,
- "lastLoginByUserMillis": 1234,
- "lastLoginByAdministratorMillis": 1234
- }
- }
-]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-until-tenant-root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-until-tenant-root.json
deleted file mode 100644
index 8c4851413a7..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-until-tenant-root.json
+++ /dev/null
@@ -1,45 +0,0 @@
-[
- {
- "tenant": "tenant1",
- "type": "ATHENS",
- "athensDomain": "domain1",
- "property": "property1",
- "applications": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1"
- }
- ],
- "metaData": {
- "createdAtMillis": 1600000000000,
- "lastDeploymentToDevMillis": 1600000000000,
- "lastSubmissionToProdMillis": 1000
- }
- },
- {
- "tenant": "tenant2",
- "type": "ATHENS",
- "athensDomain": "domain2",
- "property": "property2",
- "propertyId": "1234",
- "propertyUrl": "www.properties.tld/1234",
- "contactsUrl": "www.contacts.tld/1234",
- "issueCreationUrl": "www.issues.tld/1234",
- "contacts": [
- [
- "alice"
- ],
- [
- "bob"
- ]
- ],
- "applications": [ ],
- "metaData": {
- "createdAtMillis": 1600000000000,
- "lastLoginByUserMillis": 1234,
- "lastLoginByAdministratorMillis": 1234
- }
- }
-]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/root.json
deleted file mode 100644
index 224b38f5f19..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/root.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "resources": [
- {
- "url": "http://localhost:8080/application/v4/tenant/"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/search-deployments-multi.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/search-deployments-multi.json
deleted file mode 100644
index 8eded55ad64..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/search-deployments-multi.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "deployments": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "prod",
- "region": "us-central-1",
- "cluster": "foo",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1"
- },
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "prod",
- "region": "us-east-3",
- "cluster": "foo",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-east-3"
- },
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "prod",
- "region": "us-west-1",
- "cluster": "foo",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-west-1"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/search-deployments-single.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/search-deployments-single.json
deleted file mode 100644
index ef524c300dc..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/search-deployments-single.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "deployments": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "prod",
- "region": "us-central-1",
- "cluster": "default",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/service b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/service
deleted file mode 100644
index 834a13b8d7a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/service
+++ /dev/null
@@ -1 +0,0 @@
-path '/state/v1/' and query 'foo=bar%3F&forwarded-url=http%3A%2F%2Flocalhost%3A8080%2Fapplication%2Fv4%2Ftenant%2Ftenant1%2Fapplication%2Fapplication1%2Finstance%2Finstance1%2Fenvironment%2Fprod%2Fregion%2Fus-central-1%2Fservice%2Fstoragenode%2Fhost.com%2Fstate%2Fv1%2F' \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/staging-runs.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/staging-runs.json
deleted file mode 100644
index 6509611c3b3..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/staging-runs.json
+++ /dev/null
@@ -1,427 +0,0 @@
-{
- "runs": [
- {
- "id": 5,
- "url": "https://some.url:43/root/run/5",
- "start": 14503000,
- "end": 14503000,
- "status": "installationFailed",
- "dependent": {
- "instance": "default",
- "region": "us-east-3",
- "build": 3
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "unfinished"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "failed"
- },
- {
- "name": "startStagingSetup",
- "status": "unfinished"
- },
- {
- "name": "endStagingSetup",
- "status": "unfinished"
- },
- {
- "name": "deployReal",
- "status": "unfinished"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "startTests",
- "status": "unfinished"
- },
- {
- "name": "endTests",
- "status": "unfinished"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 4,
- "url": "https://some.url:43/root/run/4",
- "start": 14403000,
- "end": 14403000,
- "status": "installationFailed",
- "dependent": {
- "instance": "default",
- "region": "us-east-3",
- "build": 3
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "unfinished"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "failed"
- },
- {
- "name": "startStagingSetup",
- "status": "unfinished"
- },
- {
- "name": "endStagingSetup",
- "status": "unfinished"
- },
- {
- "name": "deployReal",
- "status": "unfinished"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "startTests",
- "status": "unfinished"
- },
- {
- "name": "endTests",
- "status": "unfinished"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 3,
- "url": "https://some.url:43/root/run/3",
- "start": 14403000,
- "end": 14403000,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "us-central-1",
- "build": 3
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 3,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "succeeded"
- },
- {
- "name": "startStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "endStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 2,
- "url": "https://some.url:43/root/run/2",
- "start": 1000,
- "end": 1000,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "us-central-1",
- "build": 2
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 2,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "sourcePlatform": "6.1.0",
- "sourceApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "succeeded"
- },
- {
- "name": "startStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "endStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- },
- {
- "id": 1,
- "url": "https://some.url:43/root/run/1",
- "start": 0,
- "end": 0,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "us-central-1",
- "build": 1
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployInitialReal",
- "status": "succeeded"
- },
- {
- "name": "installInitialReal",
- "status": "succeeded"
- },
- {
- "name": "startStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "endStagingSetup",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/staging-test-log.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/staging-test-log.json
deleted file mode 100644
index e825ca1f6ad..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/staging-test-log.json
+++ /dev/null
@@ -1,243 +0,0 @@
-{
- "active": false,
- "status": "installationFailed",
- "dependent": {
- "instance": "default",
- "region": "us-east-3",
- "build": 3
- },
- "log": {
- "deployTester": [
- {
- "at": 14503000,
- "type": "info",
- "message": "Deployment successful."
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "foo"
- }
- ],
- "installTester": [
- {
- "at": 14503000,
- "type": "info",
- "message": "host-tenant.application.default-t-staging.us-east-3: unorchestrated"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "host-tenant.application.default-t-staging.us-east-3: unorchestrated"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "host-tenant.application.default-t-staging.us-east-3: unorchestrated"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "host-tenant.application.default-t-staging.us-east-3: unorchestrated"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- }
- ],
- "deployInitialReal": [
- {
- "at": 14503000,
- "type": "info",
- "message": "Deploying platform version 6.1 and application build 1 ..."
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "Using CA signed certificate version 0"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "Deployment successful."
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "foo"
- }
- ],
- "installInitialReal": [
- {
- "at": 14503000,
- "type": "info",
- "message": "######## Details for all nodes ########"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "host-tenant.application.default-staging.us-east-3: unorchestrated"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 14503000,
- "type": "debug",
- "message": "host-tenant.application.default-staging.us-east-3: unorchestrated"
- },
- {
- "at": 14503000,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 14503000,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 14503000,
- "type": "info",
- "message": "Deployment expired before installation was successful."
- }
- ],
- "deactivateReal": [
- {
- "at": 14503000,
- "type": "info",
- "message": "Deactivating deployment of tenant.application in staging.us-east-3 ..."
- }
- ],
- "deactivateTester": [
- {
- "at": 14503000,
- "type": "info",
- "message": "Deactivating tester of tenant.application in staging.us-east-3 ..."
- }
- ]
- },
- "lastId": 30,
- "steps": {
- "deployTester": {
- "status": "succeeded",
- "startMillis": 14503000
- },
- "installTester": {
- "status": "unfinished",
- "startMillis": 14503000
- },
- "deployInitialReal": {
- "status": "succeeded",
- "startMillis": 14503000
- },
- "installInitialReal": {
- "status": "failed",
- "startMillis": 14503000,
- "convergence": {
- "nodes": 1,
- "down": 0,
- "needPlatformUpgrade": 0,
- "upgrading": 0,
- "needReboot": 0,
- "rebooting": 0,
- "needRestart": 0,
- "restarting": 0,
- "upgradingOs": 0,
- "upgradingFirmware": 0,
- "services": 1,
- "needNewConfig": 1,
- "retiring": 0
- }
- },
- "startStagingSetup": {
- "status": "unfinished"
- },
- "endStagingSetup": {
- "status": "unfinished"
- },
- "deployReal": {
- "status": "unfinished"
- },
- "installReal": {
- "status": "unfinished"
- },
- "startTests": {
- "status": "unfinished"
- },
- "endTests": {
- "status": "unfinished"
- },
- "copyVespaLogs": {
- "status": "succeeded",
- "startMillis": 14503000
- },
- "deactivateReal": {
- "status": "succeeded",
- "startMillis": 14503000
- },
- "deactivateTester": {
- "status": "succeeded",
- "startMillis": 14503000
- },
- "report": {
- "status": "succeeded",
- "startMillis": 14503000
- }
- },
- "vespaLogsActive": false,
- "testerLogsActive": false
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/suspended.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/suspended.json
deleted file mode 100644
index 0b855edb2b2..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/suspended.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "suspended": false
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json
deleted file mode 100644
index bb8024ff3ca..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-details.json
+++ /dev/null
@@ -1,412 +0,0 @@
-{
- "active": false,
- "status": "success",
- "dependent": {
- "instance": "instance1",
- "region": "us-central-1",
- "build": 1
- },
- "log": {
- "deployTester": [
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Deploying the tester container on platform 6.1 ..."
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Deployment successful."
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "foo"
- }
- ],
- "installTester": [
- {
- "at": 1600000000000,
- "type": "info",
- "message": "host-tenant1.application1.instance1-t-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "host-tenant1.application1.instance1-t-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "host-tenant1.application1.instance1-t-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "host-tenant1.application1.instance1-t-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "host-tenant1.application1.instance1-t-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Tester container successfully installed!"
- }
- ],
- "deployReal": [
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Deploying platform version 6.1 and application build 1 ..."
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Using CA signed certificate version 1"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Deployment successful."
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "foo"
- }
- ],
- "installReal": [
- {
- "at": 1600000000000,
- "type": "info",
- "message": "######## Details for all nodes ########"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "host-tenant1.application1.instance1-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "host-tenant1.application1.instance1-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "host-tenant1.application1.instance1-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "host-tenant1.application1.instance1-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "host-tenant1.application1.instance1-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "host-tenant1.application1.instance1-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "host-tenant1.application1.instance1-test.us-east-1: unorchestrated"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 1600000000000,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Found endpoints:"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "- test.us-east-1"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": " |-- https://instance1.application1.tenant1.us-east-1.test.vespa.oath.cloud/ (cluster 'default')"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Installation succeeded!"
- }
- ],
- "startTests": [
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Attempting to find endpoints ..."
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Found endpoints:"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "- test.us-east-1"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": " |-- https://instance1.application1.tenant1.us-east-1.test.vespa.oath.cloud/ (cluster 'default')"
- },
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Starting tests ..."
- }
- ],
- "endTests": [
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Tests completed successfully."
- }
- ],
- "deactivateReal": [
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Deactivating deployment of tenant1.application1.instance1 in test.us-east-1 ..."
- }
- ],
- "deactivateTester": [
- {
- "at": 1600000000000,
- "type": "info",
- "message": "Deactivating tester of tenant1.application1.instance1 in test.us-east-1 ..."
- }
- ]
- },
- "lastId": 67,
- "steps": {
- "deployTester": {
- "status": "succeeded",
- "startMillis": 1600000000000
- },
- "installTester": {
- "status": "succeeded",
- "startMillis": 1600000000000
- },
- "deployReal": {
- "status": "succeeded",
- "startMillis": 1600000000000
- },
- "installReal": {
- "status": "succeeded",
- "startMillis": 1600000000000
- },
- "startTests": {
- "status": "succeeded",
- "startMillis": 1600000000000
- },
- "endTests": {
- "status": "succeeded",
- "startMillis": 1600000000000
- },
- "copyVespaLogs": {
- "status": "succeeded",
- "startMillis": 1600000000000
- },
- "deactivateReal": {
- "status": "succeeded",
- "startMillis": 1600000000000
- },
- "deactivateTester": {
- "status": "succeeded",
- "startMillis": 1600000000000
- },
- "report": {
- "status": "succeeded",
- "startMillis": 1600000000000
- }
- },
- "vespaLogsActive": false,
- "testerLogsActive": false
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json
deleted file mode 100644
index 0d6ae341be4..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-job.json
+++ /dev/null
@@ -1,129 +0,0 @@
-{
- "runs": [
- {
- "id": 2,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test/run/2",
- "start": 1600000000000,
- "status": "running",
- "dependent": {
- "instance": "instance1",
- "region": "us-central-1",
- "build": 4
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 4,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "unfinished"
- },
- {
- "name": "installTester",
- "status": "unfinished"
- },
- {
- "name": "deployReal",
- "status": "unfinished"
- },
- {
- "name": "installReal",
- "status": "unfinished"
- },
- {
- "name": "startTests",
- "status": "unfinished"
- },
- {
- "name": "endTests",
- "status": "unfinished"
- },
- {
- "name": "copyVespaLogs",
- "status": "unfinished"
- },
- {
- "name": "deactivateReal",
- "status": "unfinished"
- },
- {
- "name": "deactivateTester",
- "status": "unfinished"
- },
- {
- "name": "report",
- "status": "unfinished"
- }
- ]
- },
- {
- "id": 1,
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/system-test/run/1",
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "dependent": {
- "instance": "instance1",
- "region": "us-central-1",
- "build": 1
- },
- "versions": {
- "targetPlatform": "6.1.0",
- "targetApplication": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "steps": [
- {
- "name": "deployTester",
- "status": "succeeded"
- },
- {
- "name": "installTester",
- "status": "succeeded"
- },
- {
- "name": "deployReal",
- "status": "succeeded"
- },
- {
- "name": "installReal",
- "status": "succeeded"
- },
- {
- "name": "startTests",
- "status": "succeeded"
- },
- {
- "name": "endTests",
- "status": "succeeded"
- },
- {
- "name": "copyVespaLogs",
- "status": "succeeded"
- },
- {
- "name": "deactivateReal",
- "status": "succeeded"
- },
- {
- "name": "deactivateTester",
- "status": "succeeded"
- },
- {
- "name": "report",
- "status": "succeeded"
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-log.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-log.json
deleted file mode 100644
index 17f158ac9fc..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/system-test-log.json
+++ /dev/null
@@ -1,415 +0,0 @@
-{
- "active": false,
- "status": "success",
- "dependent": {
- "instance": "default",
- "region": "us-central-1",
- "build": 1
- },
- "log": {
- "deployTester": [
- {
- "at": 0,
- "type": "info",
- "message": "Deployment successful."
- },
- {
- "at": 0,
- "type": "info",
- "message": "foo"
- }
- ],
- "installTester": [
- {
- "at": 0,
- "type": "info",
- "message": "host-tenant.application.default-t-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "host-tenant.application.default-t-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "host-tenant.application.default-t-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "host-tenant.application.default-t-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "host-tenant.application.default-t-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Tester container successfully installed!"
- }
- ],
- "deployReal": [
- {
- "at": 0,
- "type": "info",
- "message": "Deploying platform version 6.1 and application build 1 ..."
- },
- {
- "at": 0,
- "type": "info",
- "message": "Using CA signed certificate version 0"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Deployment successful."
- },
- {
- "at": 0,
- "type": "info",
- "message": "foo"
- }
- ],
- "installReal": [
- {
- "at": 0,
- "type": "info",
- "message": "######## Details for all nodes ########"
- },
- {
- "at": 0,
- "type": "info",
- "message": "host-tenant.application.default-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "info",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 0,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "host-tenant.application.default-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 0,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "host-tenant.application.default-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 0,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "host-tenant.application.default-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 0,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "host-tenant.application.default-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 0,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "host-tenant.application.default-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Waiting for convergence of 1 services across 1 nodes"
- },
- {
- "at": 0,
- "type": "info",
- "message": "1 application services still deploying"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "host-tenant.application.default-test.us-east-1: unorchestrated"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- platform vespa/vespa:6.1"
- },
- {
- "at": 0,
- "type": "debug",
- "message": "--- container on port 43 has config generation 1, wanted is 2"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Found endpoints:"
- },
- {
- "at": 0,
- "type": "info",
- "message": "- test.us-east-1"
- },
- {
- "at": 0,
- "type": "info",
- "message": " |-- https://application.tenant.us-east-1.test.vespa.oath.cloud/ (cluster 'default')"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Installation succeeded!"
- }
- ],
- "startTests": [
- {
- "at": 0,
- "type": "info",
- "message": "Attempting to find endpoints ..."
- },
- {
- "at": 0,
- "type": "info",
- "message": "Found endpoints:"
- },
- {
- "at": 0,
- "type": "info",
- "message": "- test.us-east-1"
- },
- {
- "at": 0,
- "type": "info",
- "message": " |-- https://application.tenant.us-east-1.test.vespa.oath.cloud/ (cluster 'default')"
- },
- {
- "at": 0,
- "type": "info",
- "message": "Starting tests ..."
- }
- ],
- "endTests": [
- {
- "at": 0,
- "type": "info",
- "message": "Tests completed successfully."
- }
- ],
- "deactivateReal": [
- {
- "at": 0,
- "type": "info",
- "message": "Deactivating deployment of tenant.application in test.us-east-1 ..."
- }
- ],
- "deactivateTester": [
- {
- "at": 0,
- "type": "info",
- "message": "Deactivating tester of tenant.application in test.us-east-1 ..."
- }
- ]
- },
- "lastId": 67,
- "steps": {
- "deployTester": {
- "status": "succeeded",
- "startMillis": 0
- },
- "installTester": {
- "status": "succeeded",
- "startMillis": 0
- },
- "deployReal": {
- "status": "succeeded",
- "startMillis": 0
- },
- "installReal": {
- "status": "succeeded",
- "startMillis": 0
- },
- "startTests": {
- "status": "succeeded",
- "startMillis": 0
- },
- "endTests": {
- "status": "succeeded",
- "startMillis": 0
- },
- "copyVespaLogs": {
- "status": "succeeded",
- "startMillis": 0
- },
- "deactivateReal": {
- "status": "succeeded",
- "startMillis": 0
- },
- "deactivateTester": {
- "status": "succeeded",
- "startMillis": 0
- },
- "report": {
- "status": "succeeded",
- "startMillis": 0
- }
- },
- "testReports": [
- {
- "summary": {
- "success": 1,
- "failed": 0
- }
- }
- ],
- "vespaLogsActive": false,
- "testerLogsActive": false
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-cloud.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-cloud.json
deleted file mode 100644
index c7258ab3aa6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-cloud.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "tenant": "scoober",
- "type": "CLOUD",
- "creator": "developer@scoober",
- "pemDeveloperKeys": [],
- "secretStores": [],
- "integrations": {
- "aws": {
- "tenantRole": "scoober-tenant-role",
- "accounts": []
- }
- },
- "quota": {
- "budgetUsed": 1.304
- },
- "archiveAccess": {},
- "applications": [
- {
- "tenant": "scoober",
- "application": "albums",
- "instance": "default",
- "url": "http://localhost:8080/application/v4/tenant/scoober/application/albums/instance/default"
- }
- ],
- "metaData": {
- "createdAtMillis": 1600000000000,
- "lastSubmissionToProdMillis": 1000
- },
- "cloudAccounts": [
- {
- "cloudAccount": "aws:123456789012",
- "templateVersion": "1.2.4"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-application.json
deleted file mode 100644
index 006e5158168..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-application.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "tenant": "tenant1",
- "type": "ATHENS",
- "athensDomain": "domain1",
- "property": "property1",
- "applications": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1"
- }
- ],
- "metaData": {
- "createdAtMillis": 1600000000000
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-empty-application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-empty-application.json
deleted file mode 100644
index d08cd640890..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-with-empty-application.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "tenant": "tenant1",
- "type": "ATHENS",
- "athensDomain": "domain1",
- "property": "property1",
- "applications": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1"
- }
- ],
- "metaData": {
- "createdAtMillis": 1600000000000
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-with-id.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-with-id.json
deleted file mode 100644
index 72aa268163a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications-with-id.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "tenant": "tenant2",
- "type": "ATHENS",
- "athensDomain": "domain2",
- "property": "property2",
- "propertyId": "1234",
- "applications": [ ],
- "metaData": {
- "createdAtMillis": 1600000000000
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications.json
deleted file mode 100644
index 7f94c1ea7b3..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant-without-applications.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "tenant": "tenant1",
- "type": "ATHENS",
- "athensDomain": "domain1",
- "property": "property1",
- "applications": [ ],
- "metaData": {
- "createdAtMillis": 1600000000000
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-deleted.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-deleted.json
deleted file mode 100644
index b3eabe5db96..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-deleted.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "tenant": "tenant1",
- "type": "DELETED",
- "applications": [ ],
- "metaData": {
- "createdAtMillis": 1600000000000,
- "deletedAtMillis": 1600000000000
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json
deleted file mode 100644
index edfacc7814c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json
+++ /dev/null
@@ -1,202 +0,0 @@
-{
- "tenant": "tenant1",
- "type": "ATHENS",
- "athensDomain": "domain1",
- "property": "property1",
- "applications": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "deployments": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/job/",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1",
- "projectId": 123,
- "deploying": {
- "revision": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- }
- },
- "changeBlockers": [
- {
- "versions": true,
- "revisions": false,
- "timeZone": "UTC",
- "days": [
- 1,
- 2,
- 3,
- 4,
- 5
- ],
- "hours": [
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8
- ]
- }
- ],
- "rotationId": "rotation-id-1",
- "instances": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "dev",
- "region": "us-east-1",
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://instance1.application1.tenant1.us-east-1.dev.vespa.oath.cloud/",
- "scope": "zone",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- }
- ],
- "clusters": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/dev/region/us-east-1/clusters",
- "nodes": "http://localhost:8080/zone/v2/dev/us-east-1/nodes/v2/node/?recursive=true&application=tenant1.application1.instance1",
- "yamasUrl": "http://monitoring-system.test/?environment=dev&region=us-east-1&application=tenant1.application1.instance1",
- "version": "6.1.0",
- "revision": "1.0.1",
- "build": 1,
- "deployTimeEpochMs": 1600000000000,
- "screwdriverId": "123",
- "status": "complete",
- "quota": 1.304,
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- },
- "metrics": {
- "queriesPerSecond": 1.0,
- "writesPerSecond": 2.0,
- "documentCount": 3.0,
- "queryLatencyMillis": 4.0,
- "writeLatencyMillis": 5.0,
- "lastUpdated": 123123
- }
- },
- {
- "bcpStatus": {
- "rotationStatus": "UNKNOWN"
- },
- "tenant": "tenant1",
- "application": "application1",
- "instance": "instance1",
- "environment": "prod",
- "region": "us-central-1",
- "endpoints": [
- {
- "cluster": "default",
- "tls": true,
- "url": "https://instance1.application1.tenant1.us-central-1.vespa.oath.cloud/",
- "scope": "zone",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- },
- {
- "id": "default",
- "cluster": "foo",
- "tls": true,
- "url": "https://instance1.application1.tenant1.global.vespa.oath.cloud/",
- "scope": "global",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- },
- {
- "id": "a0",
- "cluster": "foo",
- "tls": true,
- "url": "https://a0.application1.tenant1.a.vespa.oath.cloud/",
- "scope": "application",
- "routingMethod": "sharedLayer4",
- "legacy": false,
- "authMethod": "mtls"
- }
- ],
- "clusters": "http://localhost:8080/application/v4/tenant/tenant1/application/application1/instance/instance1/environment/prod/region/us-central-1/clusters",
- "nodes": "http://localhost:8080/zone/v2/prod/us-central-1/nodes/v2/node/?recursive=true&application=tenant1.application1.instance1",
- "yamasUrl": "http://monitoring-system.test/?environment=prod&region=us-central-1&application=tenant1.application1.instance1",
- "version": "6.1.0",
- "revision": "1.0.1-commit1",
- "build": 1,
- "deployTimeEpochMs": 1600000000000,
- "screwdriverId": "123",
- "endpointStatus": [
- {
- "endpointId": "default",
- "rotationId": "rotation-id-1",
- "clusterId": "foo",
- "status": "UNKNOWN",
- "lastUpdated": 0
- }
- ],
- "applicationVersion": {
- "build": 1,
- "compileVersion": "6.1.0",
- "sourceUrl": "repository1/tree/commit1",
- "commit": "commit1"
- },
- "status": "complete",
- "quota": 1.304,
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- },
- "metrics": {
- "queriesPerSecond": 1.0,
- "writesPerSecond": 2.0,
- "documentCount": 3.0,
- "queryLatencyMillis": 4.0,
- "writeLatencyMillis": 5.0,
- "lastUpdated": 123123
- }
- },
- {
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "environment": "prod",
- "region": "us-west-1"
- }
- ],
- "pemDeployKeys": [ ],
- "metrics": {
- "queryServiceQuality": 0.5,
- "writeServiceQuality": 0.7
- },
- "activity": {
- "lastQueried": 1527848130000,
- "lastWritten": 1527848130000,
- "lastQueriesPerSecond": 1.0,
- "lastWritesPerSecond": 2.0
- },
- "ownershipIssueId": "321",
- "owner": "owner-account-id",
- "deploymentIssueId": "123"
- }
- ],
- "metaData": {
- "createdAtMillis": 1600000000000,
- "lastDeploymentToDevMillis": 1600000000000,
- "lastSubmissionToProdMillis": 1000
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1.json
deleted file mode 100644
index a29e86b0aa2..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "tenant": "tenant1",
- "type": "ATHENS",
- "athensDomain": "domain2",
- "property": "property1",
- "applications": [ ],
- "metaData": {
- "createdAtMillis": 1600000000000
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json
deleted file mode 100644
index b04ac57d804..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant2.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "tenant": "tenant2",
- "type": "ATHENS",
- "athensDomain": "domain2",
- "property": "property2",
- "propertyId": "1234",
- "propertyUrl": "www.properties.tld/1234",
- "contactsUrl": "www.contacts.tld/1234",
- "issueCreationUrl": "www.issues.tld/1234",
- "contacts": [
- [
- "alice"
- ],
- [
- "bob"
- ]
- ],
- "applications": [ ],
- "metaData": {
- "createdAtMillis": 1600000000000,
- "lastLoginByUserMillis": 1234,
- "lastLoginByAdministratorMillis": 1234
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config-dev.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config-dev.json
deleted file mode 100644
index ac0f2b9f740..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config-dev.json
+++ /dev/null
@@ -1,33 +0,0 @@
-{
- "application": "tenant1:application1:my-user",
- "zone": "dev.us-east-1",
- "system": "main",
- "isCI": false,
- "platform": "6.1.0",
- "revision": 1,
- "deployedAt": 1600000000000,
- "endpoints": {
- "dev.us-east-1": [
- "https://my-user.application1.tenant1.us-east-1.dev.vespa.oath.cloud/"
- ],
- "prod.us-central-1": [
- "https://application1.tenant1.us-central-1.vespa.oath.cloud/"
- ]
- },
- "zoneEndpoints": {
- "dev.us-east-1": {
- "default": "https://my-user.application1.tenant1.us-east-1.dev.vespa.oath.cloud/"
- },
- "prod.us-central-1": {
- "default": "https://application1.tenant1.us-central-1.vespa.oath.cloud/"
- }
- },
- "clusters": {
- "dev.us-east-1": [
- "music"
- ],
- "prod.us-central-1": [
- "music"
- ]
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json
deleted file mode 100644
index 671c34cb2c0..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/test-config.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "application": "tenant1:application1:my-user",
- "zone": "prod.us-central-1",
- "system": "main",
- "isCI": false,
- "platform": "6.1.0",
- "revision": 1,
- "deployedAt": 1600000000000,
- "endpoints": {
- "prod.us-central-1": [
- "https://application1.tenant1.us-central-1.vespa.oath.cloud/"
- ]
- },
- "zoneEndpoints": {
- "prod.us-central-1": {
- "default": "https://application1.tenant1.us-central-1.vespa.oath.cloud/"
- }
- },
- "clusters": {
- "prod.us-central-1": [
- "music"
- ]
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/us-east-3-log-without-first.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/us-east-3-log-without-first.json
deleted file mode 100644
index 588f8839ab7..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/us-east-3-log-without-first.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "active": false,
- "status": "deploymentFailed",
- "log": {
- "deployReal": [
- {
- "at": 1000,
- "type": "warning",
- "message": "Failed to deploy application: ERROR!"
- }
- ]
- },
- "lastId": 1,
- "steps": {
- "deployReal": {
- "status": "failed",
- "startMillis": 1000
- },
- "installReal": {
- "status": "unfinished"
- },
- "report": {
- "status": "succeeded",
- "startMillis": 1000
- }
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/vespa.log b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/vespa.log
deleted file mode 100644
index 25916d9e6df..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/vespa.log
+++ /dev/null
@@ -1 +0,0 @@
-INFO - All good \ No newline at end of file
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java
deleted file mode 100644
index b7b8e0f8484..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/AthenzApiTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.athenz;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-
-/**
- * @author jonmv
- */
-public class AthenzApiTest extends ControllerContainerTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/";
-
- @Test
- void testAthenzApi() {
- ContainerTester tester = new ContainerTester(container, responseFiles);
- ControllerTester controllerTester = new ControllerTester(tester);
-
- controllerTester.createTenant("sandbox", AthenzApiHandler.sandboxDomainIn(tester.controller().system()), 123L);
- controllerTester.createApplication("sandbox", "app", "default");
- tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", hostedOperator.getName()));
- tester.controller().applications().createInstance(ApplicationId.from("sandbox", "app", defaultUser.getName()));
- controllerTester.createApplication("sandbox", "opp", "default");
-
- controllerTester.createTenant("tenant1", "domain1", 123L);
- controllerTester.createApplication("tenant1", "app", "default");
- tester.athenzClientFactory().getSetup().getOrCreateDomain(new AthenzDomain("domain1")).admin(defaultUser);
-
- controllerTester.createTenant("tenant2", "domain2", 123L);
- controllerTester.createApplication("tenant2", "app", "default");
-
- // GET root
- tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/"),
- new File("root.json"));
-
- // GET Athenz domains
- tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/domains"),
- new File("athensDomain-list.json"));
-
- // GET properties
- tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/properties/"),
- new File("property-list.json"));
-
- // POST user signup
- tester.assertResponse(authenticatedRequest("http://localhost:8080/athenz/v1/user", "", Request.Method.POST),
- "{\"message\":\"User 'bob' added to admin role of 'vespa.vespa.tenants.sandbox'\"}");
- }
-
-}
-
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json
deleted file mode 100644
index 9a456fd0433..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/athensDomain-list.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "data": [
- "domain1",
- "domain2",
- "vespa.vespa.tenants.sandbox"
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/property-list.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/property-list.json
deleted file mode 100644
index 2931fc8b162..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/property-list.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "properties": [
- {
- "propertyid": "1234",
- "property": "foo"
- },
- {
- "propertyid": "4321",
- "property": "bar"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/root.json
deleted file mode 100644
index 737991f29c9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/athenz/responses/root.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "resources": [
- {
- "url": "http://localhost:8080/athenz/v1/domains/"
- },
- {
- "url": "http://localhost:8080/athenz/v1/properties/"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java
deleted file mode 100644
index 2a1f6a79645..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/BillingApiHandlerV2Test.java
+++ /dev/null
@@ -1,293 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.billing;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.test.ManualClock;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.Bill;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.BillStatus;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.MockBillingController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.StatusHistory;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest;
-import com.yahoo.vespa.hosted.controller.security.Auth0Credentials;
-import com.yahoo.vespa.hosted.controller.security.CloudTenantSpec;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.math.BigDecimal;
-import java.io.File;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.ZoneOffset;
-import java.time.ZonedDateTime;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-
-/**
- * @author ogronnesby
- */
-public class BillingApiHandlerV2Test extends ControllerContainerCloudTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/";
-
- private static final TenantName tenant = TenantName.from("tenant1");
- private static final TenantName tenant2 = TenantName.from("tenant2");
- private static final Set<Role> tenantReader = Set.of(Role.reader(tenant));
- private static final Set<Role> tenantAdmin = Set.of(Role.administrator(tenant));
- private static final Set<Role> financeAdmin = Set.of(Role.hostedAccountant());
-
- private MockBillingController billingController;
- private ContainerTester tester;
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, responseFiles);
- tester.controller().tenants().create(new CloudTenantSpec(tenant, ""), new Auth0Credentials(() -> "foo", Set.of(Role.hostedOperator())));
- var clock = (ManualClock) tester.controller().serviceRegistry().clock();
- clock.setInstant(Instant.parse("2021-04-13T00:00:00Z"));
- billingController = (MockBillingController) tester.serviceRegistry().billingController();
- billingController.addBill(tenant, createBill(), true);
- }
-
- @Override
- protected String variablePartXml() {
- return " <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControlRequests'/>\n" +
- " <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControl'/>\n" +
-
- " <handler id='com.yahoo.vespa.hosted.controller.restapi.billing.BillingApiHandlerV2'>\n" +
- " <binding>http://*/billing/v2/*</binding>\n" +
- " </handler>\n" +
-
- " <http>\n" +
- " <server id='default' port='8080' />\n" +
- " <filtering>\n" +
- " <request-chain id='default'>\n" +
- " <filter id='com.yahoo.vespa.hosted.controller.restapi.filter.ControllerAuthorizationFilter'/>\n" +
- " <binding>http://*/*</binding>\n" +
- " </request-chain>\n" +
- " </filtering>\n" +
- " </http>\n";
- }
-
- @Test
- void require_tenant_info() {
- var request = request("/billing/v2/tenant/" + tenant.value()).roles(tenantReader);
- tester.assertResponse(request, "{\"tenant\":\"tenant1\",\"plan\":{\"id\":\"trial\",\"name\":\"Free Trial - for testing purposes\"},\"collection\":\"AUTO\"}");
- }
-
- @Test
- void require_accountant_for_update_collection() {
- var request = request("/billing/v2/tenant/" + tenant.value(), Request.Method.PATCH)
- .data("{\"collection\": \"INVOICE\"}");
-
- var forbidden = request.roles(tenantAdmin);
- tester.assertResponse(forbidden, """
- {
- "code" : 403,
- "message" : "Access denied"
- }""", 403);
-
- var success = request.roles(financeAdmin);
- tester.assertResponse(success, """
- {"tenant":"tenant1","plan":{"id":"trial","name":"Free Trial - for testing purposes"},"collection":"INVOICE"}""");
- }
-
- @Test
- void require_tenant_usage() {
- var request = request("/billing/v2/tenant/" + tenant + "/usage").roles(tenantReader);
- tester.assertResponse(request, "{\"from\":\"2021-04-13\",\"to\":\"2021-04-13\",\"total\":\"0.00\",\"items\":[]}");
- }
-
- @Test
- void require_tenant_invoice() {
- var listRequest = request("/billing/v2/tenant/" + tenant + "/bill").roles(tenantReader);
- tester.assertResponse(listRequest, "{\"invoices\":[{\"id\":\"id-1\",\"from\":\"2020-05-23\",\"to\":\"2020-05-28\",\"total\":\"123.00\",\"status\":\"OPEN\"}]}");
-
- var singleRequest = request("/billing/v2/tenant/" + tenant + "/bill/id-1").roles(tenantReader);
- tester.assertResponse(singleRequest, """
- {"id":"id-1","from":"2020-05-23","to":"2020-05-28","total":"123.00","status":"OPEN","statusHistory":[{"at":"2020-05-23T00:00:00Z","status":"OPEN"}],"items":[{"id":"some-id","description":"description","amount":"123.00","plan":{"id":"paid","name":"Paid Plan - for testing purposes"},"majorVersion":0,"cpu":{},"memory":{},"disk":{},"gpu":{}}]}""");
- }
-
- @Test
- void require_accountant_summary() {
- var tenantRequest = request("/billing/v2/accountant").roles(tenantReader);
- tester.assertResponse(tenantRequest, "{\n" +
- " \"code\" : 403,\n" +
- " \"message\" : \"Access denied\"\n" +
- "}", 403);
-
- var accountantRequest = request("/billing/v2/accountant").roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, """
- {"tenants":[{"tenant":"tenant1","plan":{"id":"trial","name":"Free Trial - for testing purposes"},"quota":{"budget":-1.0},"collection":"AUTO","lastBill":"1970-01-01","unbilled":"0.00"}]}""");
- }
-
- @Test
- void require_accountant_preview() {
- var accountantRequest = request("/billing/v2/accountant/preview").roles(Role.hostedAccountant());
- billingController.uncommittedBills.put(tenant, createBill());
-
- tester.assertResponse(accountantRequest, """
- {"tenants":[{"tenant":"tenant1","plan":{"id":"trial","name":"Free Trial - for testing purposes"},"quota":{"budget":-1.0},"collection":"AUTO","lastBill":"2020-05-23","unbilled":"123.00"}]}""");
- }
-
- @Test
- void require_accountant_tenant_preview() {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/preview").roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, "{\"id\":\"empty\",\"from\":\"2021-04-13\",\"to\":\"2021-04-12\",\"total\":\"0.00\",\"status\":\"OPEN\",\"statusHistory\":[{\"at\":\"2021-04-13T00:00:00Z\",\"status\":\"OPEN\"}],\"items\":[]}");
- }
-
- @Test
- void require_accountant_tenant_bill() {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/preview", Request.Method.POST)
- .roles(Role.hostedAccountant())
- .data("{\"from\": \"2020-05-01\",\"to\": \"2020-06-01\"}");
- tester.assertResponse(accountantRequest, "{\"message\":\"Created bill id-123\"}");
- }
-
- @Test
- void require_list_of_all_plans() {
- var accountantRequest = request("/billing/v2/accountant/plans")
- .roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, "{\"plans\":[{\"id\":\"trial\",\"name\":\"Free Trial - for testing purposes\"},{\"id\":\"paid\",\"name\":\"Paid Plan - for testing purposes\"},{\"id\":\"none\",\"name\":\"None Plan - for testing purposes\"}]}");
- }
-
- @Test
- void require_additional_items_empty() {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/items")
- .roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, """
- {"items":[]}""");
- }
-
- @Test
- void require_additional_items_with_content() {
- {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/items", Request.Method.POST)
- .roles(Role.hostedAccountant())
- .data("""
- {
- "description": "Additional support costs",
- "amount": "123.45"
- }""");
- tester.assertResponse(accountantRequest, """
- {"message":"Added line item for tenant tenant1"}""");
- }
-
- {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/items")
- .roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, """
- {"items":[{"id":"line-item-id","description":"Additional support costs","amount":"123.45","plan":{"id":"paid","name":"Paid Plan - for testing purposes"},"majorVersion":0,"cpu":{},"memory":{},"disk":{},"gpu":{}}]}""");
- }
-
- {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/item/line-item-id", Request.Method.DELETE)
- .roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, """
- {"message":"Successfully deleted line item line-item-id"}""");
- }
- }
-
- @Test
- void require_current_plan() {
- {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/plan")
- .roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, """
- {"id":"trial","name":"Free Trial - for testing purposes"}""");
- }
-
- {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/plan", Request.Method.POST)
- .roles(Role.hostedAccountant())
- .data("""
- {"id": "paid"}""");
- tester.assertResponse(accountantRequest, """
- {"message":"Plan: paid"}""");
- }
-
- {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/plan")
- .roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, """
- {"id":"paid","name":"Paid Plan - for testing purposes"}""");
- }
- }
-
- @Test
- void require_current_collection() {
- {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/collection")
- .roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, """
- {"collection":"AUTO"}""");
- }
-
- {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/collection", Request.Method.POST)
- .roles(Role.hostedAccountant())
- .data("""
- {"collection": "INVOICE"}""");
- tester.assertResponse(accountantRequest, """
- {"message":"Collection: INVOICE"}""");
- }
-
- {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1/collection")
- .roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, """
- {"collection":"INVOICE"}""");
- }
- }
-
- @Test
- void require_accountant_tenant() {
- var accountantRequest = request("/billing/v2/accountant/tenant/tenant1")
- .roles(Role.hostedAccountant());
- tester.assertResponse(accountantRequest, """
- {"tenant":"tenant1","plan":{"id":"trial","name":"Free Trial - for testing purposes","billed":false,"supported":false},"billing":{},"collection":"AUTO"}""");
- }
-
- @Test
- void lists_accepted_countries() {
- var req = request("/billing/v2/countries").roles(tenantReader);
- tester.assertJsonResponse(req, new File("accepted-countries.json"));
- }
-
- @Test
- void summarize_bill() {
- var req = request("/billing/v2/accountant/bill/id-1/summary?keys=plan,architecture")
- .roles(Role.hostedAccountant());
- tester.assertResponse(req, """
- {"id":"id-1","summary":[{"key":{"plan":"paid","architecture":null},"summary":{"cpu":{"cost":"0","hours":"0"},"memory":{"cost":"0","hours":"0"},"disk":{"cost":"0","hours":"0"},"gpu":{"cost":"0","hours":"0"}}}],"additional":[]}""");
- }
-
- private static Bill createBill() {
- var start = LocalDate.of(2020, 5, 23).atStartOfDay(ZoneOffset.UTC);
- var end = start.toLocalDate().plusDays(6).atStartOfDay(ZoneOffset.UTC);
- var statusHistory = new StatusHistory(new TreeMap<>(Map.of(start, BillStatus.OPEN)));
- return new Bill(
- Bill.Id.of("id-1"),
- TenantName.defaultName(),
- statusHistory,
- List.of(createLineItem(start)),
- start,
- end
- );
- }
-
- static Bill.LineItem createLineItem(ZonedDateTime addedAt) {
- return new Bill.LineItem(
- "some-id",
- "description",
- new BigDecimal("123.00"),
- "paid",
- "Smith",
- addedAt
- );
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/accepted-countries.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/accepted-countries.json
deleted file mode 100644
index 5247b84a900..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/billing/responses/accepted-countries.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "countries": [
- {
- "code": "NO",
- "name": "Norway",
- "taxIdMandatory": true,
- "taxTypes": [
- {
- "id": "no_vat",
- "description": "Norwegian VAT number",
- "pattern": "[0-9]{9}MVA",
- "example": "123456789MVA"
- }
- ]
- },
- {
- "code": "CA",
- "name": "Canada",
- "taxIdMandatory": true,
- "taxTypes": [
- {
- "id": "ca_gst_hst",
- "description": "Canadian GST/HST number",
- "pattern": "([0-9]{9}) ?RT ?([0-9]{4})",
- "example": "123456789RT0002"
- },
- {
- "id": "ca_pst_bc",
- "description": "Canadian PST number (British Columbia)",
- "pattern": "PST-?([0-9]{4})-?([0-9]{4})",
- "example": "PST-1234-5678"
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java
deleted file mode 100644
index 80368f4b134..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/ChangeManagementApiHandlerTest.java
+++ /dev/null
@@ -1,142 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.changemanagement;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequest;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.ChangeRequestSource;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.HostAction;
-import com.yahoo.vespa.hosted.controller.api.integration.vcmr.VespaChangeRequest;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.time.Instant;
-import java.time.ZonedDateTime;
-import java.util.ArrayList;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class ChangeManagementApiHandlerTest extends ControllerContainerTest {
-
- private static final String responses = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/";
- private static final AthenzIdentity operator = AthenzUser.fromUserId("operatorUser");
- private static final String changeRequestId = "id123";
-
- private ContainerTester tester;
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, responses);
- addUserToHostedOperatorRole(operator);
- tester.serviceRegistry().configServer().nodeRepository().putNodes(ZoneId.from("prod.us-east-3"), createNodes());
- tester.controller().curator().writeChangeRequest(createChangeRequest());
-
- }
-
- @Test
- void test_api() {
- assertFile(new Request("http://localhost:8080/changemanagement/v1/assessment", "{\"zone\":\"prod.us-east-3\", \"hosts\": [\"host1\"]}", Request.Method.POST), "initial.json");
- assertFile(new Request("http://localhost:8080/changemanagement/v1/assessment", "{\"zone\":\"prod.us-east-3\", \"switches\": [\"switch1\"]}", Request.Method.POST), "initial.json");
- assertFile(new Request("http://localhost:8080/changemanagement/v1/vcmr"), "vcmrs.json");
- }
-
- @Test
- void deletes_vcmr() {
- assertEquals(1, tester.controller().curator().readChangeRequests().size());
- assertFile(new Request("http://localhost:8080/changemanagement/v1/vcmr/" + changeRequestId, "", Request.Method.DELETE), "vcmr.json");
- assertEquals(0, tester.controller().curator().readChangeRequests().size());
- }
-
- @Test
- void get_vcmr() {
- assertFile(new Request("http://localhost:8080/changemanagement/v1/vcmr/" + changeRequestId, "", Request.Method.GET), "vcmr.json");
- }
-
- @Test
- void patch_vcmr() {
- var payload = "{" +
- "\"approval\": \"REJECTED\"," +
- "\"status\": \"COMPLETED\"," +
- "\"actionPlan\": {" +
- " \"hosts\": [{" +
- " \"hostname\": \"host1\"," +
- " \"state\": \"REQUIRES_OPERATOR_ACTION\"," +
- " \"lastUpdated\": \"2021-05-10T14:08:15Z\"" +
- "}]}" +
- "}";
- assertFile(new Request("http://localhost:8080/changemanagement/v1/vcmr/" + changeRequestId, payload, Request.Method.PATCH), "patched-vcmr.json");
- var changeRequest = tester.controller().curator().readChangeRequest(changeRequestId).orElseThrow();
- assertEquals(ChangeRequest.Approval.REJECTED, changeRequest.getApproval());
- assertEquals(VespaChangeRequest.Status.COMPLETED, changeRequest.getStatus());
- }
-
- private void assertFile(Request request, String filename) {
- addIdentityToRequest(request, operator);
- tester.assertResponse(request, new File(filename));
- }
-
- private VespaChangeRequest createChangeRequest() {
- var instant = Instant.ofEpochMilli(9001);
- var date = ZonedDateTime.ofInstant(instant, java.time.ZoneId.of("UTC"));
- var source = new ChangeRequestSource("aws", "id321", "url", ChangeRequestSource.Status.STARTED, date, date, "N/A");
- var actionPlan = List.of(
- new HostAction("host1", HostAction.State.RETIRING, instant),
- new HostAction("host2", HostAction.State.RETIRED, instant)
- );
-
- return new VespaChangeRequest(
- changeRequestId,
- source,
- List.of("switch1"),
- List.of("host1", "host2"),
- ChangeRequest.Approval.APPROVED,
- ChangeRequest.Impact.VERY_HIGH,
- VespaChangeRequest.Status.IN_PROGRESS,
- actionPlan,
- ZoneId.defaultId()
- );
- }
-
- private List<Node> createNodes() {
- List<Node> nodes = new ArrayList<>();
- nodes.add(createNode("node1", "host1", "default", 0 ));
- nodes.add(createNode("node2", "host1", "default", 0 ));
- nodes.add(createNode("node3", "host1", "default", 0 ));
- nodes.add(createNode("node4", "host2", "default", 0 ));
- nodes.add(createHost("host1", "switch1"));
- nodes.add(createHost("host2", "switch2"));
- return nodes;
- }
-
- private Node createNode(String nodename, String hostname, String clusterId, int group) {
- return Node.builder()
- .hostname(nodename)
- .parentHostname(hostname).state(Node.State.active)
- .owner(ApplicationId.from("mytenant", "myapp", "default"))
- .type(com.yahoo.config.provision.NodeType.tenant)
- .clusterId(clusterId)
- .group(String.valueOf(group))
- .clusterType(Node.ClusterType.content)
- .build();
- }
-
- private Node createHost(String hostname, String switchName) {
- return Node.builder()
- .hostname(hostname)
- .switchHostname(switchName)
- .owner(ApplicationId.from("mytenant", "myapp", "default"))
- .type(com.yahoo.config.provision.NodeType.host)
- .clusterId("host")
- .group("0")
- .build();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/initial.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/initial.json
deleted file mode 100644
index d9ffa9feaf9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/initial.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "assessment": {
- "updated": "2020-09-13T12:26:40Z",
- "clusters": [
- {
- "app": "mytenant:myapp:default",
- "zone": "prod.us-east-3",
- "cluster": "content:default",
- "clusterSize": 4,
- "clusterImpact": 3,
- "groupsTotal": 1,
- "groupsImpact": 1,
- "upgradePolicy": "na",
- "suggestedAction": "nothing",
- "impact": "Impact larger than upgrade policy"
- }
- ],
- "hosts": [
- {
- "hostname": "host1",
- "switchName": "switch1",
- "numberOfChildren": 3,
- "numberOfProblematicChildren": 3
- }
- ]
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/patched-vcmr.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/patched-vcmr.json
deleted file mode 100644
index b05a299bc4d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/patched-vcmr.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "id": "id123",
- "status": "COMPLETED",
- "impact": "VERY_HIGH",
- "approval": "REJECTED",
- "zoneId": "prod.default",
- "source": {
- "system": "aws",
- "id": "id321",
- "url": "url",
- "plannedStartTime": "1970-01-01T00:00:09.001Z[UTC]",
- "plannedEndTime": "1970-01-01T00:00:09.001Z[UTC]",
- "status": "STARTED",
- "category": "N/A"
- },
- "actionPlan": {
- "hosts": [
- {
- "hostname": "host1",
- "state": "REQUIRES_OPERATOR_ACTION",
- "lastUpdated": "2021-05-10T14:08:15Z"
- }
- ]
- },
- "impactedHosts": [
- "host1",
- "host2"
- ],
- "impactedSwitches": [
- "switch1"
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmr.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmr.json
deleted file mode 100644
index 531a182cad6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmr.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "id": "id123",
- "status": "IN_PROGRESS",
- "impact": "VERY_HIGH",
- "approval": "APPROVED",
- "zoneId": "prod.default",
- "source": {
- "system": "aws",
- "id": "id321",
- "url": "url",
- "plannedStartTime": "1970-01-01T00:00:09.001Z[UTC]",
- "plannedEndTime": "1970-01-01T00:00:09.001Z[UTC]",
- "status": "STARTED",
- "category": "N/A"
- },
- "actionPlan": {
- "hosts": [
- {
- "hostname": "host1",
- "state": "RETIRING",
- "lastUpdated": "1970-01-01T00:00:09.001Z"
- },
- {
- "hostname": "host2",
- "state": "RETIRED",
- "lastUpdated": "1970-01-01T00:00:09.001Z"
- }
- ]
- },
- "impactedHosts": [
- "host1",
- "host2"
- ],
- "impactedSwitches": [
- "switch1"
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmrs.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmrs.json
deleted file mode 100644
index 3a456d09bc5..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/changemanagement/responses/vcmrs.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "vcmrs": [
- {
- "id": "id123",
- "status": "IN_PROGRESS",
- "impact": "VERY_HIGH",
- "approval": "APPROVED",
- "zoneId": "prod.default",
- "source": {
- "system": "aws",
- "id": "id321",
- "url": "url",
- "plannedStartTime": "1970-01-01T00:00:09.001Z[UTC]",
- "plannedEndTime": "1970-01-01T00:00:09.001Z[UTC]",
- "status": "STARTED",
- "category": "N/A"
- },
- "actionPlan": {
- "hosts": [
- {
- "hostname": "host1",
- "state": "RETIRING",
- "lastUpdated": "1970-01-01T00:00:09.001Z"
- },
- {
- "hostname": "host2",
- "state": "RETIRED",
- "lastUpdated": "1970-01-01T00:00:09.001Z"
- }
- ]
- },
- "impactedHosts": [
- "host1",
- "host2"
- ],
- "impactedSwitches": [
- "switch1"
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java
deleted file mode 100644
index 87be4519177..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/ConfigServerApiHandlerTest.java
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.configserver;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.vespa.hosted.controller.integration.ConfigServerProxyMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.net.URI;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-
-/**
- * @author freva
- */
-public class ConfigServerApiHandlerTest extends ControllerContainerTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/responses/";
- private static final List<ZoneApi> zones = List.of(
- ZoneApiMock.fromId("prod.us-north-1"),
- ZoneApiMock.fromId("dev.aws-us-north-2"),
- ZoneApiMock.fromId("test.us-north-3"),
- ZoneApiMock.fromId("staging.us-north-4"));
-
- private ContainerTester tester;
- private ConfigServerProxyMock proxy;
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, responseFiles);
- tester.serviceRegistry().zoneRegistry()
- .setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2"))
- .setZones(zones);
- proxy = (ConfigServerProxyMock) container.components().getComponent(ConfigServerProxyMock.class.getName());
- }
-
- @Test
- void test_requests() {
- // GET /configserver/v1
- tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1"),
- new File("root.json"));
-
- // GET /configserver/v1/nodes/v2
- tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2"),
- "ok");
- assertLastRequest("https://cfg.prod.us-north-1.test.vip:4443/", "GET");
-
- // GET /configserver/v1/nodes/v2/node/?recursive=true
- tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/node/?recursive=true"),
- "ok");
- assertLastRequest("https://cfg.prod.us-north-1.test.vip:4443/", "GET");
-
- // POST /configserver/v1/dev/us-north-2/nodes/v2/command/restart?hostname=node1
- tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/dev/aws-us-north-2/nodes/v2/command/restart?hostname=node1",
- "", Request.Method.POST),
- "ok");
-
- // PUT /configserver/v1/prod/us-north-1/nodes/v2/state/dirty/node1
- tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/state/dirty/node1",
- "", Request.Method.PUT), "ok");
- assertLastRequest("https://cfg.prod.us-north-1.test.vip:4443/", "PUT");
-
- // DELETE /configserver/v1/prod/us-north-1/nodes/v2/node/node1
- tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/prod/controller/nodes/v2/node/node1",
- "", Request.Method.DELETE), "ok");
- assertLastRequest("https://localhost:4443/", "DELETE");
-
- // PATCH /configserver/v1/prod/us-north-1/nodes/v2/node/node1
- tester.assertResponse(operatorRequest("http://localhost:8080/configserver/v1/dev/aws-us-north-2/nodes/v2/node/node1",
- "{\"currentRestartGeneration\": 1}",
- Request.Method.PATCH), "ok");
- assertLastRequest("https://cfg.dev.aws-us-north-2.test.vip:4443/", "PATCH");
- assertEquals("{\"currentRestartGeneration\": 1}", proxy.lastRequestBody().get());
-
- assertFalse(tester.controller().auditLogger().readLog().entries().isEmpty(), "Actions are logged to audit log");
- }
-
- @Test
- void test_allowed_apis() {
- tester.assertResponse(() -> operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-1/"),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"Cannot access path '/' through /configserver/v1, following APIs are permitted: /flags/v1/, /nodes/v2/, /orchestrator/v1/, /state/v1/\"}",
- 403);
-
- tester.assertResponse(() -> operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-1/application/v2/tenant/vespa"),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"Cannot access path '/application/v2/tenant/vespa' through /configserver/v1, following APIs are permitted: /flags/v1/, /nodes/v2/, /orchestrator/v1/, /state/v1/\"}",
- 403);
- }
-
- @Test
- void test_invalid_requests() {
- // POST /configserver/v1/prod/us-north-34/nodes/v2
- tester.assertResponse(() -> operatorRequest("http://localhost:8080/configserver/v1/prod/us-north-42/nodes/v2",
- "", Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No such zone: prod.us-north-42\"}", 400);
- assertFalse(proxy.lastReceived().isPresent());
- }
-
- @Test
- void non_operators_are_forbidden() {
- // Read request
- tester.assertResponse(() -> authenticatedRequest("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/node"),
- "{\n" +
- " \"code\" : 403,\n" +
- " \"message\" : \"Access denied\"\n" +
- "}", 403);
-
- // Write request
- tester.assertResponse(() -> authenticatedRequest("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/node", "", Request.Method.POST),
- "{\n" +
- " \"code\" : 403,\n" +
- " \"message\" : \"Access denied\"\n" +
- "}", 403);
- }
-
- @Test
- void unauthenticated_request_are_unauthorized() {
- {
- // Read request
- Request request = new Request("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/node", "", Request.Method.GET);
- tester.assertResponse(() -> request, "{\n \"message\" : \"Not authenticated\"\n}", 401);
- }
-
- {
- // Write request
- Request request = new Request("http://localhost:8080/configserver/v1/prod/us-north-1/nodes/v2/node", "", Request.Method.POST);
- tester.assertResponse(() -> request, "{\n \"message\" : \"Not authenticated\"\n}", 401);
- }
- }
-
-
- private void assertLastRequest(String target, String method) {
- ProxyRequest last = proxy.lastReceived().orElseThrow();
- assertEquals(List.of(URI.create(target)), last.getTargets());
- assertEquals(com.yahoo.jdisc.http.HttpRequest.Method.valueOf(method), last.getMethod());
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/responses/root.json
deleted file mode 100644
index 5ccf75d2448..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/configserver/responses/root.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "zones": [
- {
- "environment": "prod",
- "region": "controller",
- "uri": "http://localhost:8080/configserver/v1/prod/controller"
- },
- {
- "environment": "prod",
- "region": "us-north-1",
- "uri": "http://localhost:8080/configserver/v1/prod/us-north-1"
- },
- {
- "environment": "dev",
- "region": "aws-us-north-2",
- "uri": "http://localhost:8080/configserver/v1/dev/aws-us-north-2"
- },
- {
- "environment": "test",
- "region": "us-north-3",
- "uri": "http://localhost:8080/configserver/v1/test/us-north-3"
- },
- {
- "environment": "staging",
- "region": "us-north-4",
- "uri": "http://localhost:8080/configserver/v1/staging/us-north-4"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java
deleted file mode 100644
index eb023aa9fe9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/ControllerApiTest.java
+++ /dev/null
@@ -1,308 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.NodeResources;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.container.jdisc.HttpRequest;
-import com.yahoo.security.KeyId;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.security.SecretSharedKey;
-import com.yahoo.security.SharedKeyGenerator;
-import com.yahoo.security.SharedKeyResealingSession;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.test.ManualClock;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.MockAccessControlService;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Application;
-import com.yahoo.vespa.hosted.controller.api.integration.resource.ResourceSnapshot;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLogger;
-import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock;
-import com.yahoo.vespa.hosted.controller.integration.SecretStoreMock;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.nio.charset.StandardCharsets;
-import java.security.KeyPair;
-import java.security.interfaces.XECPrivateKey;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-
-import static com.yahoo.security.ArrayUtils.hex;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author bratseth
- */
-public class ControllerApiTest extends ControllerContainerTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/";
-
- private ContainerTester tester;
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, responseFiles);
- }
-
- @Test
- void testControllerApi() {
- tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/", "", Request.Method.GET), new File("root.json"));
-
- ((InMemoryFlagSource) tester.controller().flagSource()).withListFlag(PermanentFlags.INACTIVE_MAINTENANCE_JOBS.id(), List.of("DeploymentExpirer"), String.class);
-
- // GET a list of all maintenance jobs
- tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/maintenance/", "", Request.Method.GET),
- new File("maintenance.json"));
- }
-
- @Test
- void testStats() {
- var mock = (NodeRepositoryMock) tester.controller().serviceRegistry().configServer().nodeRepository();
- mock.putApplication(ZoneId.from("prod", "us-west-1"),
- new Application(ApplicationId.fromFullString("t1.a1.i1"), List.of()));
- mock.putApplication(ZoneId.from("prod", "us-west-1"),
- new Application(ApplicationId.fromFullString("t2.a2.i2"), List.of()));
- mock.putApplication(ZoneId.from("prod", "us-east-3"),
- new Application(ApplicationId.fromFullString("t1.a1.i1"), List.of()));
-
- tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/stats", "", Request.Method.GET),
- new File("stats.json"));
- }
-
- @Test
- void testUpgraderApi() {
- // Get current configuration
- tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/jobs/upgrader", "", Request.Method.GET),
- "{\"upgradesPerMinute\":0.125,\"confidenceOverrides\":[]}",
- 200);
-
- // Set invalid configuration
- tester.assertResponse(
- operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"upgradesPerMinute\":-1}", Request.Method.PATCH),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Upgrades per minute must be >= 0, got -1.0\"}",
- 400);
-
- // Ignores unrecognized field
- tester.assertResponse(
- operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"foo\":\"bar\"}", Request.Method.PATCH),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No such modifiable field(s)\"}",
- 400);
-
- // Set upgrades per minute
- tester.assertResponse(
- operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader", "{\"upgradesPerMinute\":42.0}", Request.Method.PATCH),
- "{\"upgradesPerMinute\":42.0,\"confidenceOverrides\":[]}",
- 200);
-
- // Override confidence
- tester.assertResponse(
- operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.42", "broken", Request.Method.POST),
- "{\"upgradesPerMinute\":42.0,\"confidenceOverrides\":[{\"6.42.0\":\"broken\"}]}",
- 200);
-
- // Override confidence for another version
- tester.assertResponse(
- operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.43", " broken ", Request.Method.POST),
- "{\"upgradesPerMinute\":42.0,\"confidenceOverrides\":[{\"6.42.0\":\"broken\"},{\"6.43.0\":\"broken\"}]}",
- 200);
-
- // Remove first override
- tester.assertResponse(
- operatorRequest("http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.42", "", Request.Method.DELETE),
- "{\"upgradesPerMinute\":42.0,\"confidenceOverrides\":[{\"6.43.0\":\"broken\"}]}",
- 200);
-
- assertFalse(tester.controller().auditLogger().readLog().entries().isEmpty(), "Actions are logged to audit log");
- }
-
- @Test
- void testAuditLogApi() {
- ManualClock clock = new ManualClock(Instant.parse("2019-03-01T12:13:14.00Z"));
- AuditLogger logger = new AuditLogger(tester.controller().curator(), clock);
-
- // Log some operator actions
- HttpRequest req1 = HttpRequest.createTestRequest(
- "http://localhost:8080/controller/v1/maintenance/inactive/DeploymentExpirer",
- com.yahoo.jdisc.http.HttpRequest.Method.POST
- );
- req1.getJDiscRequest().setUserPrincipal(() -> "operator1");
- logger.log(req1);
-
- clock.advance(Duration.ofHours(2));
- HttpRequest req2 = HttpRequest.createTestRequest(
- "http://localhost:8080/controller/v1/jobs/upgrader/confidence/6.42",
- com.yahoo.jdisc.http.HttpRequest.Method.POST,
- new ByteArrayInputStream("broken".getBytes(StandardCharsets.UTF_8))
- );
- req2.getJDiscRequest().setUserPrincipal(() -> "operator2");
- logger.log(req2);
-
- // Verify log
- tester.assertResponse(authenticatedRequest("http://localhost:8080/controller/v1/auditlog/"), new File("auditlog.json"));
- }
-
- @Test
- void testMeteringApi() {
- ApplicationId applicationId = ApplicationId.from("tenant", "app", "instance");
- Instant timestamp = Instant.ofEpochMilli(123456789);
- ZoneId zoneId = ZoneId.defaultId();
- var resources = List.of(
- new NodeResources(12, 48, 1200, 0, NodeResources.DiskSpeed.any, NodeResources.StorageType.any, NodeResources.Architecture.arm64),
- new NodeResources(24, 96, 2400, 0, NodeResources.DiskSpeed.any, NodeResources.StorageType.any, NodeResources.Architecture.x86_64));
-
- var snapshots = resources.stream().map(x -> new ResourceSnapshot(applicationId, x, timestamp, zoneId, 0, CloudAccount.empty)).toList();
-
- tester.controller().serviceRegistry().resourceDatabase().writeResourceSnapshots(snapshots);
- tester.assertResponse(
- operatorRequest("http://localhost:8080/controller/v1/metering/tenant/tenantName/month/2020-02", "", Request.Method.GET),
- new File("metering.json")
- );
- }
-
- @Test
- void testApproveMembership() {
- ApplicationId applicationId = ApplicationId.from("tenant", "app", "instance");
- DeploymentId deployment = new DeploymentId(applicationId, ZoneId.defaultId());
- String requestBody = "{\n" +
- " \"applicationId\": \"" + deployment.applicationId().serializedForm() + "\",\n" +
- " \"zone\": \"" + deployment.zoneId().value() + "\"\n" +
- "}";
-
- MockAccessControlService accessControlService = (MockAccessControlService) tester.serviceRegistry().accessControlService();
- tester.assertResponse(operatorRequest("http://localhost:8080/controller/v1/access/requests/" + hostedOperator.getName(), requestBody, Request.Method.POST),
- "{\"message\":\"Unable to approve membership request\"}", 400);
-
- accessControlService.addPendingMember(hostedOperator);
- tester.assertResponse(operatorRequest("http://localhost:8080/controller/v1/access/requests/" + hostedOperator.getName(), requestBody, Request.Method.POST),
- "{\"message\":\"Unable to approve membership request\"}", 400);
-
- tester.controller().supportAccess().allow(deployment, tester.controller().clock().instant().plus(Duration.ofHours(1)), "tenantx");
- tester.assertResponse(operatorRequest("http://localhost:8080/controller/v1/access/requests/" + hostedOperator.getName(), requestBody, Request.Method.POST),
- "{\"members\":[\"user.alice\"]}");
- }
-
- private SharedKeyResealingSession.ResealingResponse extractResealingResponseFromJsonResponse(String json) {
- var cursor = SlimeUtils.jsonToSlime(json).get();
- var responseField = cursor.field("resealResponse");
- if (!responseField.valid()) {
- fail("No 'resealResponse' field in JSON response");
- }
- return SharedKeyResealingSession.ResealingResponse.fromSerializedString(responseField.asString());
- }
-
- private record ResealingTestData(SharedKeyResealingSession.ResealingRequest resealingRequest,
- SharedKeyResealingSession session,
- SecretSharedKey originalSecretSharedKey,
- KeyPair originalReceiverKeyPair) {}
-
- private static ResealingTestData createResealingRequestData(String keyIdStr) {
- var receiverKeyPair = KeyUtils.generateX25519KeyPair();
- var keyId = KeyId.ofString(keyIdStr);
- var sharedKey = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), keyId);
-
- var session = SharedKeyResealingSession.newEphemeralSession();
- var resealRequest = session.resealingRequestFor(sharedKey.sealedSharedKey());
- return new ResealingTestData(resealRequest, session, sharedKey, receiverKeyPair);
- }
-
- private static String requestJsonOf(ResealingTestData reqData) {
- return "{\"resealRequest\":\"%s\"}".formatted(reqData.resealingRequest.toSerializedString());
- }
-
- @Test
- void decryption_token_reseal_request_succeeds_when_matching_versioned_key_found() {
- var reqData = createResealingRequestData("a.really.cool.key.123"); // Must match key name in config
- var secret = hex(reqData.originalSecretSharedKey.secretKey().getEncoded());
-
- var secretStore = (SecretStoreMock)tester.controller().secretStore();
- secretStore.setSecret("a.really.cool.key", KeyUtils.toBase58EncodedX25519PrivateKey((XECPrivateKey)reqData.originalReceiverKeyPair.getPrivate()), 123);
-
- tester.assertResponse(
- () -> operatorRequest("http://localhost:8080/controller/v1/access/cores/reseal", requestJsonOf(reqData), Request.Method.POST),
- (responseJson) -> {
- var resealResponse = extractResealingResponseFromJsonResponse(responseJson.getBodyAsString());
- var myShared = reqData.session.openResealingResponse(resealResponse);
- assertEquals(secret, hex(reqData.originalSecretSharedKey.secretKey().getEncoded()));
- },
- 200);
- }
-
- @Test
- void decryption_token_reseal_request_fails_when_unexpected_key_name_is_supplied() {
- var reqData = createResealingRequestData("a.really.cool.but.non.existing.key.123");
- tester.assertResponse(
- () -> operatorRequest("http://localhost:8080/controller/v1/access/cores/reseal", requestJsonOf(reqData), Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Token is not generated for the expected key\"}",
- 400);
- }
-
- @Test
- void secret_key_lookup_does_not_use_key_id_provided_in_user_supplied_token() {
- var reqData = createResealingRequestData("a.sneaky.key.123");
- var secretStore = (SecretStoreMock)tester.controller().secretStore();
- // Token key ID is technically valid, but should not be used. Only config should be obeyed.
- secretStore.setSecret("a.sneaky.key", KeyUtils.toBase58EncodedX25519PrivateKey((XECPrivateKey)reqData.originalReceiverKeyPair.getPrivate()), 123);
-
- tester.assertResponse(
- () -> operatorRequest("http://localhost:8080/controller/v1/access/cores/reseal", requestJsonOf(reqData), Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Token is not generated for the expected key\"}",
- 400);
- }
-
- @Test
- void decryption_token_reseal_request_fails_when_request_payload_is_missing_or_bogus() {
- tester.assertResponse(
- () -> operatorRequest("http://localhost:8080/controller/v1/access/cores/reseal", "{}", Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Expected field \\\"resealRequest\\\" in request\"}",
- 400);
- // TODO this error message is technically an implementation detail...
- tester.assertResponse(
- () -> operatorRequest("http://localhost:8080/controller/v1/access/cores/reseal",
- "{\"resealRequest\":\"five badgers destroying a flowerbed\"}", Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Input character not part of codec alphabet\"}",
- 400);
- }
-
- @Test
- void decryption_token_reseal_request_fails_when_key_id_does_not_conform_to_expected_form() {
- tester.assertResponse(
- () -> operatorRequest("http://localhost:8080/controller/v1/access/cores/reseal",
- requestJsonOf(createResealingRequestData("a-really-cool-key")), Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Key ID is not of the form 'name.version'\"}",
- 400);
- tester.assertResponse(
- () -> operatorRequest("http://localhost:8080/controller/v1/access/cores/reseal",
- requestJsonOf(createResealingRequestData("a.really.cool.key.123asdf")), Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Key version is not a valid integer\"}",
- 400);
- tester.assertResponse(
- () -> operatorRequest("http://localhost:8080/controller/v1/access/cores/reseal",
- requestJsonOf(createResealingRequestData("a.really.cool.key.")), Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Key version is not a valid integer\"}",
- 400);
- tester.assertResponse(
- () -> operatorRequest("http://localhost:8080/controller/v1/access/cores/reseal",
- requestJsonOf(createResealingRequestData("a.really.cool.key.-123")), Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Key version is out of range\"}",
- 400);
- tester.assertResponse(
- () -> operatorRequest("http://localhost:8080/controller/v1/access/cores/reseal",
- requestJsonOf(createResealingRequestData("a.really.cool.key.%d".formatted((long)Integer.MAX_VALUE + 1))), Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Key version is not a valid integer\"}",
- 400);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandlerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandlerTest.java
deleted file mode 100644
index ffdf6796d9d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/WellKnownApiHandlerTest.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.controller;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-/**
- * @author olaa
- */
-class WellKnownApiHandlerTest extends ControllerContainerTest {
-
- private ContainerTester tester;
- private final String SECURITY_TXT = "Mocked security txt";
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, "src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/");
- }
-
- @Test
- void securityTxt() {
- tester.assertResponse(new Request("http://localhost:8080/.well-known/security.txt"), SECURITY_TXT);
- }
-
- @Override
- protected String variablePartXml() {
- return String.format("""
- <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControlRequests'/>
- <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControl'/>
- <handler id="com.yahoo.vespa.hosted.controller.restapi.controller.WellKnownApiHandler" bundle="controller-clients" >
- <config name="vespa.hosted.controller.config.well-known-folder">
- <securityTxt>%s</securityTxt>
- </config>
- <binding>http://*/.well-known/*</binding>
- </handler>
- <http>
- <server id='default' port='8080' />
- <filtering>
- <request-chain id='default'>
- <filter id='com.yahoo.jdisc.http.filter.security.misc.NoopFilter'/>
- <binding>http://*/*</binding>
- </request-chain>
- </filtering>
- </http>
- """, SECURITY_TXT);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/auditlog.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/auditlog.json
deleted file mode 100644
index b04e34daaa9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/auditlog.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "entries": [
- {
- "time": "2019-03-01T14:13:14Z",
- "client": "other",
- "user": "operator2",
- "method": "POST",
- "resource": "/controller/v1/jobs/upgrader/confidence/6.42",
- "data": "broken"
- },
- {
- "time": "2019-03-01T12:13:14Z",
- "client": "other",
- "user": "operator1",
- "method": "POST",
- "resource": "/controller/v1/maintenance/inactive/DeploymentExpirer"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
deleted file mode 100644
index e7f48bf4ffa..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json
+++ /dev/null
@@ -1,142 +0,0 @@
-{
- "jobs": [
- {
- "name": "ApplicationMetaDataGarbageCollector"
- },
- {
- "name": "ApplicationOwnershipConfirmer"
- },
- {
- "name": "ArchiveAccessMaintainer"
- },
- {
- "name": "ArchiveUriUpdater"
- },
- {
- "name": "ArtifactExpirer"
- },
- {
- "name": "AwsOsUpgrader"
- },
- {
- "name": "BcpGroupUpdater"
- },
- {
- "name": "BillingDatabaseMaintainer"
- },
- {
- "name": "BillingReportMaintainer"
- },
- {
- "name": "CertificatePoolMaintainer"
- },
- {
- "name": "ChangeRequestMaintainer"
- },
- {
- "name": "CloudAccountVerifier"
- },
- {
- "name": "CloudDatabaseMaintainer"
- },
- {
- "name": "CloudTrialExpirer"
- },
- {
- "name": "ContactInformationMaintainer"
- },
- {
- "name": "CostReportMaintainer"
- },
- {
- "name": "DataPlaneTokenRedeployer"
- },
- {
- "name": "DefaultOsUpgrader"
- },
- {
- "name": "DeploymentExpirer"
- },
- {
- "name": "DeploymentInfoMaintainer"
- },
- {
- "name": "DeploymentIssueReporter"
- },
- {
- "name": "DeploymentMetricsMaintainer"
- },
- {
- "name": "DeploymentUpgrader"
- },
- {
- "name": "EnclaveAccessMaintainer"
- },
- {
- "name": "EndpointCertificateMaintainer"
- },
- {
- "name": "HostInfoUpdater"
- },
- {
- "name": "JobRunner"
- },
- {
- "name": "MeteringMonitorMaintainer"
- },
- {
- "name": "MetricsReporter"
- },
- {
- "name": "NameServiceDispatcher"
- },
- {
- "name": "OsUpgradeScheduler"
- },
- {
- "name": "OsVersionStatusUpdater"
- },
- {
- "name": "OutstandingChangeDeployer"
- },
- {
- "name": "ReadyJobsTrigger"
- },
- {
- "name": "ReindexingTriggerer"
- },
- {
- "name": "ResourceMeterMaintainer"
- },
- {
- "name": "ResourceTagMaintainer"
- },
- {
- "name": "RetriggerMaintainer"
- },
- {
- "name": "SystemUpgrader"
- },
- {
- "name": "TenantRoleCleanupMaintainer"
- },
- {
- "name": "TenantRoleMaintainer"
- },
- {
- "name": "Upgrader"
- },
- {
- "name": "UserManagementMaintainer"
- },
- {
- "name": "VcmrMaintainer"
- },
- {
- "name": "VersionStatusUpdater"
- }
- ],
- "inactive": [
- "DeploymentExpirer"
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/metering.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/metering.json
deleted file mode 100644
index 475bf1d449a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/metering.json
+++ /dev/null
@@ -1,26 +0,0 @@
-[
- {
- "applicationId": "tenant.app.instance",
- "timestamp": 123456789,
- "zoneId": "prod.default",
- "cpu": 12.0,
- "memory": 48.0,
- "disk": 1200.0,
- "architecture": "arm64",
- "version": 0,
- "gpuMemoryGb": 0.0,
- "gpuCount": 0
- },
- {
- "applicationId": "tenant.app.instance",
- "timestamp": 123456789,
- "zoneId": "prod.default",
- "cpu": 24.0,
- "memory": 96.0,
- "disk": 2400.0,
- "architecture": "x86_64",
- "version": 0,
- "gpuMemoryGb": 0.0,
- "gpuCount": 0
- }
-]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/root.json
deleted file mode 100644
index 6d800db303c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/root.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "resources": [
- {
- "url": "http://localhost:8080/controller/v1/auditlog/"
- },
- {
- "url": "http://localhost:8080/controller/v1/maintenance/"
- },
- {
- "url": "http://localhost:8080/controller/v1/stats/"
- },
- {
- "url": "http://localhost:8080/controller/v1/jobs/upgrader/"
- },
- {
- "url": "http://localhost:8080/controller/v1/metering/tenant/"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/stats.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/stats.json
deleted file mode 100644
index 44b52e5be2c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/stats.json
+++ /dev/null
@@ -1,68 +0,0 @@
-{
- "zones": [
- {
- "id": "prod.us-east-3",
- "totalCost": 0.0,
- "totalAllocatedCost": 0.0,
- "load": {
- "cpu": 0.0,
- "memory": 0.0,
- "disk": 0.0
- },
- "activeLoad": {
- "cpu": 0.0,
- "memory": 0.0,
- "disk": 0.0
- },
- "applications": [
- {
- "id": "t1.a1.i1",
- "load": {
- "cpu": 0.0,
- "memory": 0.0,
- "disk": 0.0
- },
- "cost": 0.0,
- "unutilizedCost": 0.0
- }
- ]
- },
- {
- "id": "prod.us-west-1",
- "totalCost": 0.0,
- "totalAllocatedCost": 0.0,
- "load": {
- "cpu": 0.0,
- "memory": 0.0,
- "disk": 0.0
- },
- "activeLoad": {
- "cpu": 0.0,
- "memory": 0.0,
- "disk": 0.0
- },
- "applications": [
- {
- "id": "t1.a1.i1",
- "load": {
- "cpu": 0.0,
- "memory": 0.0,
- "disk": 0.0
- },
- "cost": 0.0,
- "unutilizedCost": 0.0
- },
- {
- "id": "t2.a2.i2",
- "load": {
- "cpu": 0.0,
- "memory": 0.0,
- "disk": 0.0
- },
- "cost": 0.0,
- "unutilizedCost": 0.0
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenServiceTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenServiceTest.java
deleted file mode 100644
index 87facaf1218..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/dataplanetoken/DataplaneTokenServiceTest.java
+++ /dev/null
@@ -1,225 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.dataplanetoken;
-
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneToken;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.DataplaneTokenVersions;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.FingerPrint;
-import com.yahoo.vespa.hosted.controller.api.integration.dataplanetoken.TokenId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.restapi.dataplanetoken.DataplaneTokenService.State;
-import org.junit.jupiter.api.Test;
-
-import java.security.Principal;
-import java.time.Duration;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeMap;
-
-import static java.util.stream.Collectors.toMap;
-import static java.util.stream.Collectors.toSet;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-public class DataplaneTokenServiceTest {
-
- private final ControllerTester tester = new ControllerTester(SystemName.Public);
- private final DataplaneTokenService dataplaneTokenService = new DataplaneTokenService(tester.controller());
- private final TenantName tenantName = TenantName.from("tenant");
- private final Principal principal = new SimplePrincipal("user");
- private final TokenId tokenId = TokenId.of("myTokenId");
- private final Map<HostName, Map<TokenId, List<FingerPrint>>> activeTokens = tester.configServer().activeTokenFingerprints(null);
-
- @Test
- void triggers_token_redeployments() {
- DeploymentTester deploymentTester = new DeploymentTester(tester);
- DeploymentContext app = deploymentTester.newDeploymentContext(tenantName.value(), "app", "default");
- ApplicationPackage appPackage = new ApplicationPackageBuilder().region("aws-us-east-1c")
- .container("default", AuthMethod.token, AuthMethod.token)
- .build();
- app.submit(appPackage).deploy();
-
- // First token version is added after deployment, so re-trigger.
- dataplaneTokenService.triggerTokenChangeDeployments();
- assertEquals(List.of(), deploymentTester.jobs().active());
- FingerPrint print1 = dataplaneTokenService.generateToken(tenantName, TokenId.of("token-1"), null, principal).fingerPrint();
- dataplaneTokenService.triggerTokenChangeDeployments();
- app.runJob(JobType.prod("aws-us-east-1c"));
- assertEquals(List.of(), deploymentTester.jobs().active());
-
- // New token version is added, so re-trigger.
- tester.clock().advance(Duration.ofSeconds(1));
- dataplaneTokenService.triggerTokenChangeDeployments();
- assertEquals(List.of(), deploymentTester.jobs().active());
- FingerPrint print2 = dataplaneTokenService.generateToken(tenantName, TokenId.of("token-1"), null, principal).fingerPrint();
- dataplaneTokenService.triggerTokenChangeDeployments();
- app.runJob(JobType.prod("aws-us-east-1c"));
- assertEquals(List.of(), deploymentTester.jobs().active());
-
- // Another token version is added, so re-trigger.
- tester.clock().advance(Duration.ofSeconds(1));
- dataplaneTokenService.triggerTokenChangeDeployments();
- assertEquals(List.of(), deploymentTester.jobs().active());
- FingerPrint print3 = dataplaneTokenService.generateToken(tenantName, TokenId.of("token-1"), tester.clock().instant().plusSeconds(10), principal).fingerPrint();
- dataplaneTokenService.triggerTokenChangeDeployments();
- app.runJob(JobType.prod("aws-us-east-1c"));
- assertEquals(List.of(), deploymentTester.jobs().active());
-
- // An expired token version is deleted, so do _not_ re-trigger.
- tester.clock().advance(Duration.ofSeconds(11));
- dataplaneTokenService.triggerTokenChangeDeployments();
- assertEquals(List.of(), deploymentTester.jobs().active());
- dataplaneTokenService.deleteToken(tenantName, TokenId.of("token-1"), print3);
- dataplaneTokenService.triggerTokenChangeDeployments();
- assertEquals(List.of(), deploymentTester.jobs().active());
-
- // Some unused token version is added, so do _not_ re-trigger.
- tester.clock().advance(Duration.ofSeconds(1));
- dataplaneTokenService.triggerTokenChangeDeployments();
- assertEquals(List.of(), deploymentTester.jobs().active());
- dataplaneTokenService.generateToken(tenantName, TokenId.of("token-3"), null, principal);
- dataplaneTokenService.triggerTokenChangeDeployments();
- assertEquals(List.of(), deploymentTester.jobs().active());
-
- // One token version is deleted, so re-trigger.
- tester.clock().advance(Duration.ofSeconds(1));
- dataplaneTokenService.triggerTokenChangeDeployments();
- assertEquals(List.of(), deploymentTester.jobs().active());
- dataplaneTokenService.deleteToken(tenantName, TokenId.of("token-1"), print2);
- dataplaneTokenService.triggerTokenChangeDeployments();
- app.runJob(JobType.prod("aws-us-east-1c"));
- assertEquals(List.of(), deploymentTester.jobs().active());
-
- // Last token version is deleted, the token is no longer known, so re-trigger.
- tester.clock().advance(Duration.ofSeconds(1));
- dataplaneTokenService.triggerTokenChangeDeployments();
- assertEquals(List.of(), deploymentTester.jobs().active());
- dataplaneTokenService.deleteToken(tenantName, TokenId.of("token-1"), print1);
- dataplaneTokenService.triggerTokenChangeDeployments();
- app.runJob(JobType.prod("aws-us-east-1c"));
- assertEquals(List.of(), deploymentTester.jobs().active());
- }
-
- @Test
- void computes_aggregate_state() {
- DeploymentTester deploymentTester = new DeploymentTester(tester);
- DeploymentContext app = deploymentTester.newDeploymentContext(tenantName.value(), "app", "default");
- app.submit().deploy();
-
- TokenId[] id = new TokenId[5];
- FingerPrint[][] print = new FingerPrint[5][3];
- for (int i = 0; i < id.length; i++) {
- id[i] = TokenId.of("id" + i);
- for (int j = 0; j < 3; j++) {
- print[i][j] = dataplaneTokenService.generateToken(tenantName, id[i], null, principal).fingerPrint();
- }
- }
- for (int j = 0; j < 2; j++) {
- dataplaneTokenService.deleteToken(tenantName, id[2], print[2][j]);
- dataplaneTokenService.deleteToken(tenantName, id[4], print[4][j]);
- }
- for (int j = 0; j < 3; j++) {
- dataplaneTokenService.deleteToken(tenantName, id[3], print[3][j]);
- }
- // "host1" has all versions of all current tokens, except the first versions of tokens 1 and 2.
- activeTokens.put(HostName.of("host1"),
- Map.of(id[0], List.of(print[0]),
- id[1], List.of(print[1][1], print[1][2]),
- id[2], List.of(print[2][1], print[2][2])));
- // "host2" has all versions of all current tokens, except the last version of token 1.
- activeTokens.put(HostName.of("host2"),
- Map.of(id[0], List.of(print[0]),
- id[1], List.of(print[1][0], print[1][1]),
- id[2], List.of(print[2])));
- // "host3" has no current tokens at all, but has the last version of token 3
- activeTokens.put(HostName.of("host3"),
- Map.of(id[3], List.of(print[3][2])));
-
- // All fingerprints of token 0 are active on all hosts where token 0 is found, so they are all active.
- // The first and last fingerprints of token 1 are missing from one host each, so these are activating.
- // The first fingerprints of token 2 are no longer current, but the second is found on a host; both deactivating.
- // The whole of token 3 is forgotten, but the last fingerprint is found on a host; deactivating.
- // Only the last fingerprint of token 4 remains, but this token is not used anywhere; unused.
- assertEquals(new TreeMap<>(Map.of(id[0], new TreeMap<>(Map.of(print[0][0], State.ACTIVE,
- print[0][1], State.ACTIVE,
- print[0][2], State.ACTIVE)),
- id[1], new TreeMap<>(Map.of(print[1][0], State.DEPLOYING,
- print[1][1], State.ACTIVE,
- print[1][2], State.DEPLOYING)),
- id[2], new TreeMap<>(Map.of(print[2][0], State.REVOKING,
- print[2][1], State.REVOKING,
- print[2][2], State.ACTIVE)),
- id[3], new TreeMap<>(Map.of(print[3][2], State.REVOKING)),
- id[4], new TreeMap<>(Map.of(print[4][2], State.UNUSED)))),
- new TreeMap<>(dataplaneTokenService.listTokensWithState(tenantName).entrySet().stream()
- .collect(toMap(tokens -> tokens.getKey().tokenId(),
- tokens -> new TreeMap<>(tokens.getValue())))));
- }
-
- @Test
- void generates_and_persists_token() {
- DataplaneToken dataplaneToken = dataplaneTokenService.generateToken(tenantName, tokenId, tester.clock().instant().plus(Duration.ofDays(100)), principal);
- List<DataplaneTokenVersions> dataplaneTokenVersions = dataplaneTokenService.listTokens(tenantName);
- assertEquals(dataplaneToken.fingerPrint(), dataplaneTokenVersions.get(0).tokenVersions().get(0).fingerPrint());
- assertEquals(dataplaneToken.expiration(), dataplaneTokenVersions.get(0).tokenVersions().get(0).expiration());
- }
-
- @Test
- void generating_new_token_appends() {
- DataplaneToken dataplaneToken1 = dataplaneTokenService.generateToken(tenantName, tokenId, tester.clock().instant().plus(Duration.ofDays(1)), principal);
- DataplaneToken dataplaneToken2 = dataplaneTokenService.generateToken(tenantName, tokenId, null, principal);
- assertNotEquals(dataplaneToken1.fingerPrint(), dataplaneToken2.fingerPrint());
-
- List<DataplaneTokenVersions> dataplaneTokenVersions = dataplaneTokenService.listTokens(tenantName);
- Set<FingerPrint> tokenFingerprints = dataplaneTokenVersions.stream()
- .filter(token -> token.tokenId().equals(tokenId))
- .map(DataplaneTokenVersions::tokenVersions)
- .flatMap(Collection::stream)
- .map(DataplaneTokenVersions.Version::fingerPrint)
- .collect(toSet());
- assertEquals(tokenFingerprints, Set.of(dataplaneToken1.fingerPrint(), dataplaneToken2.fingerPrint()));
- }
-
- @Test
- void delete_last_fingerprint_deletes_token() {
- DataplaneToken dataplaneToken1 = dataplaneTokenService.generateToken(tenantName, tokenId, null, principal);
- DataplaneToken dataplaneToken2 = dataplaneTokenService.generateToken(tenantName, tokenId, null, principal);
- dataplaneTokenService.deleteToken(tenantName, tokenId, dataplaneToken1.fingerPrint());
- dataplaneTokenService.deleteToken(tenantName, tokenId, dataplaneToken2.fingerPrint());
- assertEquals(List.of(), dataplaneTokenService.listTokens(tenantName));
- }
-
- @Test
- void deleting_nonexistent_fingerprint_throws() {
- DataplaneToken dataplaneToken = dataplaneTokenService.generateToken(tenantName, tokenId, null, principal);
- DataplaneToken dataplaneToken2 = dataplaneTokenService.generateToken(tenantName, tokenId, null, principal);
- dataplaneTokenService.deleteToken(tenantName, tokenId, dataplaneToken.fingerPrint());
-
- // Token currently contains value of "dataplaneToken2"
- IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> dataplaneTokenService.deleteToken(tenantName, tokenId, dataplaneToken.fingerPrint()));
- assertEquals("Fingerprint does not exist: " + dataplaneToken.fingerPrint(), exception.getMessage());
- }
-
- @Test
- void deleting_nonexistent_token_throws() {
- DataplaneToken dataplaneToken = dataplaneTokenService.generateToken(tenantName, tokenId, null, principal);
- dataplaneTokenService.deleteToken(tenantName, tokenId, dataplaneToken.fingerPrint());
-
- // Token is created and deleted above, no longer exists
- IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> dataplaneTokenService.deleteToken(tenantName, tokenId, dataplaneToken.fingerPrint()));
- assertEquals("Token does not exist: " + tokenId, exception.getMessage());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java
deleted file mode 100644
index cfacbfe77f4..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/BadgeApiTest.java
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.deployment;
-
-import com.yahoo.application.container.handler.Request.Method;
-import com.yahoo.component.Version;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-
-/**
- * @author jonmv
- */
-public class BadgeApiTest extends ControllerContainerTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/";
-
- @Test
- void testBadgeApi() throws IOException {
- ContainerTester tester = new ContainerTester(container, responseFiles);
- DeploymentTester deploymentTester = new DeploymentTester(new ControllerTester(tester));
- deploymentTester.controllerTester().upgradeSystem(Version.fromString("6.1"));
- var application = deploymentTester.newDeploymentContext("tenant", "application", "default");
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder().parallel("us-west-1", "aws-us-east-1a")
- .test("us-west-1")
- .region("ap-southeast-1")
- .test("ap-southeast-1")
- .region("eu-west-1")
- .test("eu-west-1")
- .build();
- application.submit(applicationPackage).deploy();
- application.submit(applicationPackage)
- .runJob(DeploymentContext.systemTest)
- .runJob(DeploymentContext.stagingTest)
- .runJob(DeploymentContext.productionUsWest1)
- .runJob(DeploymentContext.productionAwsUsEast1a)
- .runJob(DeploymentContext.testUsWest1)
- .runJob(DeploymentContext.productionApSoutheast1)
- .failDeployment(DeploymentContext.testApSoutheast1);
- application.submit(applicationPackage)
- .failTests(DeploymentContext.systemTest, true)
- .runJob(DeploymentContext.stagingTest);
- for (int i = 0; i < 31; i++)
- if ((i & 1) == 0)
- application.failDeployment(DeploymentContext.productionUsWest1);
- else
- application.triggerJobs().abortJob(DeploymentContext.productionUsWest1);
- application.triggerJobs();
- tester.controller().applications().deploymentTrigger().reTrigger(application.instanceId(), DeploymentContext.systemTest, "reason");
- tester.controller().applications().deploymentTrigger().reTrigger(application.instanceId(), DeploymentContext.testEuWest1, "reason");
-
- tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default"),
- Files.readString(Paths.get(responseFiles + "overview.svg")), 200);
- tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/production-us-west-1?historyLength=0"),
- Files.readString(Paths.get(responseFiles + "single-running.svg")), 200);
- tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/system-test"),
- Files.readString(Paths.get(responseFiles + "running-test.svg")), 200);
- tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/production-us-west-1?historyLength=32"),
- Files.readString(Paths.get(responseFiles + "history.svg")), 200);
-
- // New change not reflected before cache entry expires.
- tester.serviceRegistry().clock().advance(Duration.ofSeconds(59));
- application.runJob(DeploymentContext.productionUsWest1);
- tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/production-us-west-1?historyLength=32"),
- Files.readString(Paths.get(responseFiles + "history.svg")), 200);
-
- // Cached entry refreshed after a minute.
- tester.serviceRegistry().clock().advance(Duration.ofSeconds(1));
- tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/production-us-west-1?historyLength=32"),
- Files.readString(Paths.get(responseFiles + "history2.svg")), 200);
-
- tester.assertResponse(authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default/production-us-west-1?historyLength=0"),
- Files.readString(Paths.get(responseFiles + "single-done.svg")), 200);
-
- tester.assertResponse(() -> authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default", "", Method.HEAD),
- __ -> { },
- 200);
-
- tester.assertResponse(() -> authenticatedRequest("http://localhost:8080/badge/v1/tenant/application/default", "", Method.OPTIONS),
- response -> Assertions.assertEquals(List.of(Map.entry("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS"),
- Map.entry("Access-Control-Allow-Origin", "*"),
- Map.entry("Allow", "GET, HEAD, OPTIONS"),
- Map.entry("Content-Type", "image/svg+xml; charset=UTF-8")),
- response.getHeaders().entries()),
- 200);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java
deleted file mode 100644
index 5d0608b8bd9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/DeploymentApiTest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.deployment;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.config.provision.CloudAccount;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import com.yahoo.vespa.hosted.controller.versions.NodeVersion;
-import com.yahoo.vespa.hosted.controller.versions.VersionStatus;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Optional;
-
-/**
- * @author bratseth
- */
-public class DeploymentApiTest extends ControllerContainerTest {
-
- private final static String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/";
-
- @Test
- void testDeploymentApi() {
- ContainerTester tester = new ContainerTester(container, responseFiles);
- DeploymentTester deploymentTester = new DeploymentTester(new ControllerTester(tester));
-
- CloudAccount cloudAccount = CloudAccount.from("aws:123456789012");
- deploymentTester.controllerTester().flagSource().withListFlag(PermanentFlags.CLOUD_ACCOUNTS.id(), List.of(cloudAccount.value()), String.class);
- deploymentTester.controllerTester().zoneRegistry().configureCloudAccount(cloudAccount, ZoneId.from("prod.aws-us-east-1a"));
- Version version = Version.fromString("4.9");
- deploymentTester.controllerTester().upgradeSystem(version);
- ApplicationPackage multiInstancePackage = new ApplicationPackageBuilder()
- .instances("i1,i2")
- .region("us-west-1")
- .build();
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("aws-us-east-1a", cloudAccount.value())
- .build();
- ApplicationPackage emptyPackage = new ApplicationPackageBuilder().instances("default")
- .allow(ValidationId.deploymentRemoval)
- .build();
-
- // Deploy application without any declared jobs on the oldest version.
- var oldAppWithoutDeployment = deploymentTester.newDeploymentContext("tenant4", "application4", "default");
- oldAppWithoutDeployment.submit().failDeployment(DeploymentContext.systemTest);
- oldAppWithoutDeployment.submit(emptyPackage);
-
- // System upgrades to 5.0 for the other applications.
- version = Version.fromString("5.0");
- deploymentTester.controllerTester().upgradeSystem(version);
-
- // 3 applications deploy on current system version.
- var failingApp = deploymentTester.newDeploymentContext("tenant1", "application1", "default");
- var productionApp = deploymentTester.newDeploymentContext("tenant2", "application2", "i1");
- var otherProductionApp = deploymentTester.newDeploymentContext("tenant2", "application2", "i2");
- var appWithoutDeployments = deploymentTester.newDeploymentContext("tenant3", "application3", "default");
- failingApp.submit(applicationPackage).deploy();
- productionApp.submit(multiInstancePackage).runJob(DeploymentContext.systemTest).runJob(DeploymentContext.stagingTest).runJob(DeploymentContext.productionUsWest1);
- otherProductionApp.runJob(DeploymentContext.productionUsWest1);
-
- // Deploy once so that job information is stored, then remove the deployment by submitting an empty deployment spec.
- appWithoutDeployments.submit(applicationPackage).deploy();
- appWithoutDeployments.submit(new ApplicationPackageBuilder().allow(ValidationId.deploymentRemoval).build());
-
- // New version released
- version = Version.fromString("5.1");
- deploymentTester.controllerTester().upgradeSystem(version);
-
- // Applications upgrade, 1/2 succeed
- deploymentTester.upgrader().maintain();
- deploymentTester.triggerJobs();
- productionApp.runJob(DeploymentContext.systemTest).runJob(DeploymentContext.stagingTest).runJob(DeploymentContext.productionUsWest1);
- failingApp.failDeployment(DeploymentContext.systemTest).failDeployment(DeploymentContext.stagingTest).timeOutConvergence(DeploymentContext.stagingTest);
- deploymentTester.upgrader().maintain();
- deploymentTester.triggerJobs();
-
- // Application fails application change
- productionApp.submit(multiInstancePackage).failDeployment(DeploymentContext.systemTest);
-
- tester.controller().updateVersionStatus(censorConfigServers(VersionStatus.compute(tester.controller())));
- tester.assertResponse(operatorRequest("http://localhost:8080/deployment/v1/"), new File("root.json"));
- }
-
- private VersionStatus censorConfigServers(VersionStatus versionStatus) {
- List<VespaVersion> censored = new ArrayList<>();
- for (VespaVersion version : versionStatus.versions()) {
- if (version.nodeVersions().size() > 0) {
- version = new VespaVersion(version.versionNumber(),
- version.releaseCommit(),
- version.committedAt(),
- version.isControllerVersion(),
- version.isSystemVersion(),
- version.isReleased(),
- List.of(new NodeVersion(HostName.of("config1.test"), ZoneId.defaultId(), version.versionNumber(), version.versionNumber(), Optional.empty()),
- new NodeVersion(HostName.of("config2.test"), ZoneId.defaultId(), version.versionNumber(), version.versionNumber(), Optional.empty())),
- version.confidence()
- );
- }
- censored.add(version);
- }
- return new VersionStatus(censored, versionStatus.currentMajor());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history.svg b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history.svg
deleted file mode 100644
index c0566ade33a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history.svg
+++ /dev/null
@@ -1,183 +0,0 @@
-<svg xmlns='http://www.w3.org/2000/svg' width='659.058159125822' height='20' role='img' aria-label='Deployment Status'>
- <title>Deployment Status</title>
- <linearGradient id='light' x2='0' y2='100%'>
- <stop offset='0' stop-color='#fff' stop-opacity='.5'/>
- <stop offset='.1' stop-color='#fff' stop-opacity='.15'/>
- <stop offset='.9' stop-color='#000' stop-opacity='.15'/>
- <stop offset='1' stop-color='#000' stop-opacity='.5'/>
- </linearGradient>
- <linearGradient id='left-light' x2='100%' y2='0'>
- <stop offset='0' stop-color='#fff' stop-opacity='.3'/>
- <stop offset='.5' stop-color='#fff' stop-opacity='.1'/>
- <stop offset='1' stop-color='#fff' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='right-shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.0'/>
- <stop offset='.5' stop-color='#000' stop-opacity='.1'/>
- <stop offset='1' stop-color='#000' stop-opacity='.3'/>
- </linearGradient>
- <linearGradient id='shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#222' stop-opacity='.3'/>
- <stop offset='.625' stop-color='#555' stop-opacity='.3'/>
- <stop offset='.9' stop-color='#555' stop-opacity='.05'/>
- <stop offset='1' stop-color='#555' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='shade' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.20'/>
- <stop offset='0.05' stop-color='#000' stop-opacity='.10'/>
- <stop offset='1' stop-color='#000' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bf103c' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-warning' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bd890b' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#00f844' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <clipPath id='rounded'>
- <rect width='659.058159125822' height='20' rx='3' fill='#fff'/>
- </clipPath>
- <g clip-path='url(#rounded)'>
- <rect x='653.26809109179' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='646.1879570885093' rx='3' width='13.080134003280707' height='20' fill='#00f844'/>
- <rect x='646.1879570885093' rx='3' width='13.080134003280707' height='20' fill='url(#shade)'/>
- <rect x='646.4043039211865' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='639.1078230852286' rx='3' width='13.296480835957981' height='20' fill='#00f844'/>
- <rect x='639.1078230852286' rx='3' width='13.296480835957981' height='20' fill='url(#shade)'/>
- <rect x='639.3307808029524' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='631.8113422492706' rx='3' width='13.519438553681752' height='20' fill='#bf103c'/>
- <rect x='631.8113422492706' rx='3' width='13.519438553681752' height='20' fill='url(#shade)'/>
- <rect x='632.0411128600877' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='624.2919036955889' rx='3' width='13.749209164498811' height='20' fill='#bd890b'/>
- <rect x='624.2919036955889' rx='3' width='13.749209164498811' height='20' fill='url(#shade)'/>
- <rect x='624.5286953802824' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='616.5426945310901' rx='3' width='13.986000849192376' height='20' fill='#bf103c'/>
- <rect x='616.5426945310901' rx='3' width='13.986000849192376' height='20' fill='url(#shade)'/>
- <rect x='616.7867218317995' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='608.5566936818977' rx='3' width='14.230028149901685' height='20' fill='#bd890b'/>
- <rect x='608.5566936818977' rx='3' width='14.230028149901685' height='20' fill='url(#shade)'/>
- <rect x='608.8081776965013' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='600.326665531996' rx='3' width='14.48151216450522' height='20' fill='#bf103c'/>
- <rect x='600.326665531996' rx='3' width='14.48151216450522' height='20' fill='url(#shade)'/>
- <rect x='600.5858341144344' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='591.8451533674908' rx='3' width='14.740680746943664' height='20' fill='#bd890b'/>
- <rect x='591.8451533674908' rx='3' width='14.740680746943664' height='20' fill='url(#shade)'/>
- <rect x='592.1122413342111' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='583.1044726205471' rx='3' width='15.007768713664104' height='20' fill='#bf103c'/>
- <rect x='583.1044726205471' rx='3' width='15.007768713664104' height='20' fill='url(#shade)'/>
- <rect x='583.3797219632555' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='574.096703906883' rx='3' width='15.283018056372542' height='20' fill='#bd890b'/>
- <rect x='574.096703906883' rx='3' width='15.283018056372542' height='20' fill='url(#shade)'/>
- <rect x='574.380364011798' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='564.8136858505105' rx='3' width='15.566678161287442' height='20' fill='#bf103c'/>
- <rect x='564.8136858505105' rx='3' width='15.566678161287442' height='20' fill='url(#shade)'/>
- <rect x='565.1060137243161' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='555.2470076892231' rx='3' width='15.859006035092985' height='20' fill='#bd890b'/>
- <rect x='555.2470076892231' rx='3' width='15.859006035092985' height='20' fill='url(#shade)'/>
- <rect x='555.5482681919269' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='545.3880016541301' rx='3' width='16.16026653779677' height='20' fill='#bf103c'/>
- <rect x='545.3880016541301' rx='3' width='16.16026653779677' height='20' fill='url(#shade)'/>
- <rect x='545.6984677390362' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='535.2277351163333' rx='3' width='16.470732622702883' height='20' fill='#bd890b'/>
- <rect x='535.2277351163333' rx='3' width='16.470732622702883' height='20' fill='url(#shade)'/>
- <rect x='535.5476880773482' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='524.7570024936304' rx='3' width='16.790685583717845' height='20' fill='#bf103c'/>
- <rect x='524.7570024936304' rx='3' width='16.790685583717845' height='20' fill='url(#shade)'/>
- <rect x='525.0867322201259' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='513.9663169099125' rx='3' width='17.12041531021341' height='20' fill='#bd890b'/>
- <rect x='513.9663169099125' rx='3' width='17.12041531021341' height='20' fill='url(#shade)'/>
- <rect x='514.3061221493763' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='502.84590159969906' rx='3' width='17.460220549677203' height='20' fill='#bf103c'/>
- <rect x='502.84590159969906' rx='3' width='17.460220549677203' height='20' fill='url(#shade)'/>
- <rect x='503.196090228411' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='491.38568105002184' rx='3' width='17.810409178389147' height='20' fill='#bd890b'/>
- <rect x='491.38568105002184' rx='3' width='17.810409178389147' height='20' fill='url(#shade)'/>
- <rect x='491.7465703520016' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='479.5752718716327' rx='3' width='18.171298480368904' height='20' fill='#bf103c'/>
- <rect x='479.5752718716327' rx='3' width='18.171298480368904' height='20' fill='url(#shade)'/>
- <rect x='479.9471888261109' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='467.40397339126383' rx='3' width='18.543215434847077' height='20' fill='#bd890b'/>
- <rect x='467.40397339126383' rx='3' width='18.543215434847077' height='20' fill='url(#shade)'/>
- <rect x='467.7872549689374' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='454.86075795641676' rx='3' width='18.92649701252067' height='20' fill='#bf103c'/>
- <rect x='454.86075795641676' rx='3' width='18.92649701252067' height='20' fill='url(#shade)'/>
- <rect x='455.25575142475725' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='441.9342609438961' rx='3' width='19.32149048086113' height='20' fill='#bd890b'/>
- <rect x='441.9342609438961' rx='3' width='19.32149048086113' height='20' fill='url(#shade)'/>
- <rect x='442.3413241817867' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='428.61277046303496' rx='3' width='19.728553718751726' height='20' fill='#bf103c'/>
- <rect x='428.61277046303496' rx='3' width='19.728553718751726' height='20' fill='url(#shade)'/>
- <rect x='429.03227228502243' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='414.8842167442832' rx='3' width='20.148055540739207' height='20' fill='#bd890b'/>
- <rect x='414.8842167442832' rx='3' width='20.148055540739207' height='20' fill='url(#shade)'/>
- <rect x='415.31653723473755' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='400.736161203544' rx='3' width='20.58037603119359' height='20' fill='#bf103c'/>
- <rect x='400.736161203544' rx='3' width='20.58037603119359' height='20' fill='url(#shade)'/>
- <rect x='401.1816920610293' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='386.1557851723504' rx='3' width='21.025906888678875' height='20' fill='#bd890b'/>
- <rect x='386.1557851723504' rx='3' width='21.025906888678875' height='20' fill='url(#shade)'/>
- <rect x='386.61493006451815' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='371.12987828367153' rx='3' width='21.485051780846597' height='20' fill='#bf103c'/>
- <rect x='371.12987828367153' rx='3' width='21.485051780846597' height='20' fill='url(#shade)'/>
- <rect x='371.60305321299876' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='355.6448265028249' rx='3' width='21.958226710173836' height='20' fill='#bd890b'/>
- <rect x='355.6448265028249' rx='3' width='21.958226710173836' height='20' fill='url(#shade)'/>
- <rect x='356.13246018352817' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='339.68659979265107' rx='3' width='22.445860390877083' height='20' fill='#bf103c'/>
- <rect x='339.68659979265107' rx='3' width='22.445860390877083' height='20' fill='url(#shade)'/>
- <rect x='340.18913403911733' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='323.24073940177396' rx='3' width='22.948394637343355' height='20' fill='#bd890b'/>
- <rect x='323.24073940177396' rx='3' width='22.948394637343355' height='20' fill='url(#shade)'/>
- <rect x='323.7586295288612' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='306.2923447644306' rx='3' width='23.466284764430597' height='20' fill='#bf103c'/>
- <rect x='306.2923447644306' rx='3' width='23.466284764430597' height='20' fill='url(#shade)'/>
- <rect x='306.82606' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='288.82606' rx='3' width='24.0' height='20' fill='#bd890b'/>
- <rect x='288.82606' rx='3' width='24.0' height='20' fill='url(#shade)'/>
- <rect x='288.82606' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='163.18729000000002' rx='3' width='131.63876999999997' height='20' fill='url(#run-on-failure)'/>
- <rect x='163.18729000000002' rx='3' width='131.63876999999997' height='20' fill='url(#shade)'/>
- <rect width='169.18729000000002' height='20' fill='#404040'/>
- <rect x='-6.0' rx='3' width='175.18729000000002' height='20' fill='url(#shade)'/>
- <rect width='2' height='20' fill='url(#left-light)'/>
- <rect x='657.058159125822' width='2' height='20' fill='url(#right-shadow)'/>
- <rect width='659.058159125822' height='20' fill='url(#light)'/>
- </g>
- <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='11'>
- <svg x='6.5' y='3.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <svg x='6.0' y='2.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'>
- <stop offset='0.01' stop-color='#c6783e'/>
- <stop offset='0.54' stop-color='#ff9750'/>
- </linearGradient>
- <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'>
- <stop offset='0' stop-color='#005a8e'/>
- <stop offset='0.54' stop-color='#1a7db6'/>
- </linearGradient>
- <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <text font-size='11' x='96.09364500000001' y='15' fill='#000' fill-opacity='.4' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='95.59364500000001' y='14' fill='#fff' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='232.506675' y='15' fill='#000' fill-opacity='.4' textLength='113.63876999999998'>production-us-west-1</text>
- <text font-size='11' x='232.006675' y='14' fill='#fff' textLength='113.63876999999998'>production-us-west-1</text>
- </g>
-</svg>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history2.svg b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history2.svg
deleted file mode 100644
index e527fa8d80f..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/history2.svg
+++ /dev/null
@@ -1,183 +0,0 @@
-<svg xmlns='http://www.w3.org/2000/svg' width='659.058159125822' height='20' role='img' aria-label='Deployment Status'>
- <title>Deployment Status</title>
- <linearGradient id='light' x2='0' y2='100%'>
- <stop offset='0' stop-color='#fff' stop-opacity='.5'/>
- <stop offset='.1' stop-color='#fff' stop-opacity='.15'/>
- <stop offset='.9' stop-color='#000' stop-opacity='.15'/>
- <stop offset='1' stop-color='#000' stop-opacity='.5'/>
- </linearGradient>
- <linearGradient id='left-light' x2='100%' y2='0'>
- <stop offset='0' stop-color='#fff' stop-opacity='.3'/>
- <stop offset='.5' stop-color='#fff' stop-opacity='.1'/>
- <stop offset='1' stop-color='#fff' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='right-shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.0'/>
- <stop offset='.5' stop-color='#000' stop-opacity='.1'/>
- <stop offset='1' stop-color='#000' stop-opacity='.3'/>
- </linearGradient>
- <linearGradient id='shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#222' stop-opacity='.3'/>
- <stop offset='.625' stop-color='#555' stop-opacity='.3'/>
- <stop offset='.9' stop-color='#555' stop-opacity='.05'/>
- <stop offset='1' stop-color='#555' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='shade' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.20'/>
- <stop offset='0.05' stop-color='#000' stop-opacity='.10'/>
- <stop offset='1' stop-color='#000' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bf103c' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-warning' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bd890b' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#00f844' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <clipPath id='rounded'>
- <rect width='659.058159125822' height='20' rx='3' fill='#fff'/>
- </clipPath>
- <g clip-path='url(#rounded)'>
- <rect x='653.26809109179' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='646.1879570885093' rx='3' width='13.080134003280707' height='20' fill='#00f844'/>
- <rect x='646.1879570885093' rx='3' width='13.080134003280707' height='20' fill='url(#shade)'/>
- <rect x='646.4043039211865' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='639.1078230852286' rx='3' width='13.296480835957981' height='20' fill='#bf103c'/>
- <rect x='639.1078230852286' rx='3' width='13.296480835957981' height='20' fill='url(#shade)'/>
- <rect x='639.3307808029524' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='631.8113422492706' rx='3' width='13.519438553681752' height='20' fill='#bd890b'/>
- <rect x='631.8113422492706' rx='3' width='13.519438553681752' height='20' fill='url(#shade)'/>
- <rect x='632.0411128600877' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='624.2919036955889' rx='3' width='13.749209164498811' height='20' fill='#bf103c'/>
- <rect x='624.2919036955889' rx='3' width='13.749209164498811' height='20' fill='url(#shade)'/>
- <rect x='624.5286953802824' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='616.5426945310901' rx='3' width='13.986000849192376' height='20' fill='#bd890b'/>
- <rect x='616.5426945310901' rx='3' width='13.986000849192376' height='20' fill='url(#shade)'/>
- <rect x='616.7867218317995' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='608.5566936818977' rx='3' width='14.230028149901685' height='20' fill='#bf103c'/>
- <rect x='608.5566936818977' rx='3' width='14.230028149901685' height='20' fill='url(#shade)'/>
- <rect x='608.8081776965013' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='600.326665531996' rx='3' width='14.48151216450522' height='20' fill='#bd890b'/>
- <rect x='600.326665531996' rx='3' width='14.48151216450522' height='20' fill='url(#shade)'/>
- <rect x='600.5858341144344' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='591.8451533674908' rx='3' width='14.740680746943664' height='20' fill='#bf103c'/>
- <rect x='591.8451533674908' rx='3' width='14.740680746943664' height='20' fill='url(#shade)'/>
- <rect x='592.1122413342111' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='583.1044726205471' rx='3' width='15.007768713664104' height='20' fill='#bd890b'/>
- <rect x='583.1044726205471' rx='3' width='15.007768713664104' height='20' fill='url(#shade)'/>
- <rect x='583.3797219632555' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='574.096703906883' rx='3' width='15.283018056372542' height='20' fill='#bf103c'/>
- <rect x='574.096703906883' rx='3' width='15.283018056372542' height='20' fill='url(#shade)'/>
- <rect x='574.380364011798' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='564.8136858505105' rx='3' width='15.566678161287442' height='20' fill='#bd890b'/>
- <rect x='564.8136858505105' rx='3' width='15.566678161287442' height='20' fill='url(#shade)'/>
- <rect x='565.1060137243161' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='555.2470076892231' rx='3' width='15.859006035092985' height='20' fill='#bf103c'/>
- <rect x='555.2470076892231' rx='3' width='15.859006035092985' height='20' fill='url(#shade)'/>
- <rect x='555.5482681919269' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='545.3880016541301' rx='3' width='16.16026653779677' height='20' fill='#bd890b'/>
- <rect x='545.3880016541301' rx='3' width='16.16026653779677' height='20' fill='url(#shade)'/>
- <rect x='545.6984677390362' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='535.2277351163333' rx='3' width='16.470732622702883' height='20' fill='#bf103c'/>
- <rect x='535.2277351163333' rx='3' width='16.470732622702883' height='20' fill='url(#shade)'/>
- <rect x='535.5476880773482' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='524.7570024936304' rx='3' width='16.790685583717845' height='20' fill='#bd890b'/>
- <rect x='524.7570024936304' rx='3' width='16.790685583717845' height='20' fill='url(#shade)'/>
- <rect x='525.0867322201259' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='513.9663169099125' rx='3' width='17.12041531021341' height='20' fill='#bf103c'/>
- <rect x='513.9663169099125' rx='3' width='17.12041531021341' height='20' fill='url(#shade)'/>
- <rect x='514.3061221493763' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='502.84590159969906' rx='3' width='17.460220549677203' height='20' fill='#bd890b'/>
- <rect x='502.84590159969906' rx='3' width='17.460220549677203' height='20' fill='url(#shade)'/>
- <rect x='503.196090228411' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='491.38568105002184' rx='3' width='17.810409178389147' height='20' fill='#bf103c'/>
- <rect x='491.38568105002184' rx='3' width='17.810409178389147' height='20' fill='url(#shade)'/>
- <rect x='491.7465703520016' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='479.5752718716327' rx='3' width='18.171298480368904' height='20' fill='#bd890b'/>
- <rect x='479.5752718716327' rx='3' width='18.171298480368904' height='20' fill='url(#shade)'/>
- <rect x='479.9471888261109' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='467.40397339126383' rx='3' width='18.543215434847077' height='20' fill='#bf103c'/>
- <rect x='467.40397339126383' rx='3' width='18.543215434847077' height='20' fill='url(#shade)'/>
- <rect x='467.7872549689374' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='454.86075795641676' rx='3' width='18.92649701252067' height='20' fill='#bd890b'/>
- <rect x='454.86075795641676' rx='3' width='18.92649701252067' height='20' fill='url(#shade)'/>
- <rect x='455.25575142475725' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='441.9342609438961' rx='3' width='19.32149048086113' height='20' fill='#bf103c'/>
- <rect x='441.9342609438961' rx='3' width='19.32149048086113' height='20' fill='url(#shade)'/>
- <rect x='442.3413241817867' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='428.61277046303496' rx='3' width='19.728553718751726' height='20' fill='#bd890b'/>
- <rect x='428.61277046303496' rx='3' width='19.728553718751726' height='20' fill='url(#shade)'/>
- <rect x='429.03227228502243' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='414.8842167442832' rx='3' width='20.148055540739207' height='20' fill='#bf103c'/>
- <rect x='414.8842167442832' rx='3' width='20.148055540739207' height='20' fill='url(#shade)'/>
- <rect x='415.31653723473755' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='400.736161203544' rx='3' width='20.58037603119359' height='20' fill='#bd890b'/>
- <rect x='400.736161203544' rx='3' width='20.58037603119359' height='20' fill='url(#shade)'/>
- <rect x='401.1816920610293' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='386.1557851723504' rx='3' width='21.025906888678875' height='20' fill='#bf103c'/>
- <rect x='386.1557851723504' rx='3' width='21.025906888678875' height='20' fill='url(#shade)'/>
- <rect x='386.61493006451815' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='371.12987828367153' rx='3' width='21.485051780846597' height='20' fill='#bd890b'/>
- <rect x='371.12987828367153' rx='3' width='21.485051780846597' height='20' fill='url(#shade)'/>
- <rect x='371.60305321299876' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='355.6448265028249' rx='3' width='21.958226710173836' height='20' fill='#bf103c'/>
- <rect x='355.6448265028249' rx='3' width='21.958226710173836' height='20' fill='url(#shade)'/>
- <rect x='356.13246018352817' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='339.68659979265107' rx='3' width='22.445860390877083' height='20' fill='#bd890b'/>
- <rect x='339.68659979265107' rx='3' width='22.445860390877083' height='20' fill='url(#shade)'/>
- <rect x='340.18913403911733' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='323.24073940177396' rx='3' width='22.948394637343355' height='20' fill='#bf103c'/>
- <rect x='323.24073940177396' rx='3' width='22.948394637343355' height='20' fill='url(#shade)'/>
- <rect x='323.7586295288612' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='306.2923447644306' rx='3' width='23.466284764430597' height='20' fill='#bd890b'/>
- <rect x='306.2923447644306' rx='3' width='23.466284764430597' height='20' fill='url(#shade)'/>
- <rect x='306.82606' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='288.82606' rx='3' width='24.0' height='20' fill='#bf103c'/>
- <rect x='288.82606' rx='3' width='24.0' height='20' fill='url(#shade)'/>
- <rect x='288.82606' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='163.18729000000002' rx='3' width='131.63876999999997' height='20' fill='#00f844'/>
- <rect x='163.18729000000002' rx='3' width='131.63876999999997' height='20' fill='url(#shade)'/>
- <rect width='169.18729000000002' height='20' fill='#404040'/>
- <rect x='-6.0' rx='3' width='175.18729000000002' height='20' fill='url(#shade)'/>
- <rect width='2' height='20' fill='url(#left-light)'/>
- <rect x='657.058159125822' width='2' height='20' fill='url(#right-shadow)'/>
- <rect width='659.058159125822' height='20' fill='url(#light)'/>
- </g>
- <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='11'>
- <svg x='6.5' y='3.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <svg x='6.0' y='2.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'>
- <stop offset='0.01' stop-color='#c6783e'/>
- <stop offset='0.54' stop-color='#ff9750'/>
- </linearGradient>
- <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'>
- <stop offset='0' stop-color='#005a8e'/>
- <stop offset='0.54' stop-color='#1a7db6'/>
- </linearGradient>
- <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <text font-size='11' x='96.09364500000001' y='15' fill='#000' fill-opacity='.4' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='95.59364500000001' y='14' fill='#fff' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='232.506675' y='15' fill='#000' fill-opacity='.4' textLength='113.63876999999998'>production-us-west-1</text>
- <text font-size='11' x='232.006675' y='14' fill='#fff' textLength='113.63876999999998'>production-us-west-1</text>
- </g>
-</svg>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg
deleted file mode 100644
index 46e4acaace6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/overview.svg
+++ /dev/null
@@ -1,119 +0,0 @@
-<svg xmlns='http://www.w3.org/2000/svg' width='689.25265' height='20' role='img' aria-label='Deployment Status'>
- <title>Deployment Status</title>
- <linearGradient id='light' x2='0' y2='100%'>
- <stop offset='0' stop-color='#fff' stop-opacity='.5'/>
- <stop offset='.1' stop-color='#fff' stop-opacity='.15'/>
- <stop offset='.9' stop-color='#000' stop-opacity='.15'/>
- <stop offset='1' stop-color='#000' stop-opacity='.5'/>
- </linearGradient>
- <linearGradient id='left-light' x2='100%' y2='0'>
- <stop offset='0' stop-color='#fff' stop-opacity='.3'/>
- <stop offset='.5' stop-color='#fff' stop-opacity='.1'/>
- <stop offset='1' stop-color='#fff' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='right-shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.0'/>
- <stop offset='.5' stop-color='#000' stop-opacity='.1'/>
- <stop offset='1' stop-color='#000' stop-opacity='.3'/>
- </linearGradient>
- <linearGradient id='shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#222' stop-opacity='.3'/>
- <stop offset='.625' stop-color='#555' stop-opacity='.3'/>
- <stop offset='.9' stop-color='#555' stop-opacity='.05'/>
- <stop offset='1' stop-color='#555' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='shade' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.20'/>
- <stop offset='0.05' stop-color='#000' stop-opacity='.10'/>
- <stop offset='1' stop-color='#000' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bf103c' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-warning' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bd890b' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#00f844' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <clipPath id='rounded'>
- <rect width='689.25265' height='20' rx='3' fill='#fff'/>
- </clipPath>
- <g clip-path='url(#rounded)'>
- <rect x='683.25265' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='651.06202' rx='3' width='38.19063' height='20' fill='url(#run-on-success)'/>
- <polygon points='561.318755 0 561.318755 20 660.06202 20 668.06202 0' fill='#00f844'/>
- <rect x='561.318755' rx='3' width='131.74345499999998' height='20' fill='url(#shade)'/>
- <rect x='561.318755' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='529.128125' rx='3' width='38.19063' height='20' fill='#bf103c'/>
- <polygon points='412.452885 0 412.452885 20 538.128125 20 546.128125 0' fill='#00f844'/>
- <rect x='412.452885' rx='3' width='158.67543' height='20' fill='url(#shade)'/>
- <rect x='412.452885' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='274.336835' rx='3' width='144.11604999999997' height='20' fill='url(#run-on-success)'/>
- <rect x='284.336835' rx='3' width='134.11604999999997' height='20' fill='url(#shade)'/>
- <rect x='284.336835' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='252.146205' rx='3' width='38.19063' height='20' fill='#00f844'/>
- <polygon points='163.18729000000002 0 163.18729000000002 20 261.146205 20 269.146205 0' fill='url(#run-on-failure)'/>
- <rect x='163.18729000000002' rx='3' width='130.959105' height='20' fill='url(#shade)'/>
- <rect width='169.18729000000002' height='20' fill='#404040'/>
- <rect x='-6.0' rx='3' width='175.18729000000002' height='20' fill='url(#shade)'/>
- <rect width='2' height='20' fill='url(#left-light)'/>
- <rect x='687.25265' width='2' height='20' fill='url(#right-shadow)'/>
- <rect width='689.25265' height='20' fill='url(#light)'/>
- </g>
- <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='11'>
- <svg x='6.5' y='3.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <svg x='6.0' y='2.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'>
- <stop offset='0.01' stop-color='#c6783e'/>
- <stop offset='0.54' stop-color='#ff9750'/>
- </linearGradient>
- <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'>
- <stop offset='0' stop-color='#005a8e'/>
- <stop offset='0.54' stop-color='#1a7db6'/>
- </linearGradient>
- <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <text font-size='11' x='96.09364500000001' y='15' fill='#000' fill-opacity='.4' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='95.59364500000001' y='14' fill='#fff' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='202.07825250000002' y='15' fill='#000' fill-opacity='.4' textLength='52.781925'>us-west-1</text>
- <text font-size='11' x='201.57825250000002' y='14' fill='#fff' textLength='52.781925'>us-west-1</text>
- <text font-size='9' x='248.55771000000001' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text>
- <text font-size='9' x='248.05771000000001' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text>
- <text font-size='9' x='276.74152000000004' y='15' fill='#000' fill-opacity='.4' textLength='16.190630000000002'>test</text>
- <text font-size='9' x='276.24152000000004' y='14' fill='#fff' textLength='16.190630000000002'>test</text>
- <text font-size='11' x='337.806365' y='15' fill='#000' fill-opacity='.4' textLength='81.93905999999998'>aws-us-east-1a</text>
- <text font-size='11' x='337.306365' y='14' fill='#fff' textLength='81.93905999999998'>aws-us-east-1a</text>
- <text font-size='9' x='398.86439' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text>
- <text font-size='9' x='398.36439' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text>
- <text font-size='11' x='465.20201' y='15' fill='#000' fill-opacity='.4' textLength='80.49825'>ap-southeast-1</text>
- <text font-size='11' x='464.70201' y='14' fill='#fff' textLength='80.49825'>ap-southeast-1</text>
- <text font-size='9' x='525.53963' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text>
- <text font-size='9' x='525.03963' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text>
- <text font-size='9' x='553.72344' y='15' fill='#000' fill-opacity='.4' textLength='16.190630000000002'>test</text>
- <text font-size='9' x='553.22344' y='14' fill='#fff' textLength='16.190630000000002'>test</text>
- <text font-size='11' x='600.6018925' y='15' fill='#000' fill-opacity='.4' textLength='53.566275'>eu-west-1</text>
- <text font-size='11' x='600.1018925' y='14' fill='#fff' textLength='53.566275'>eu-west-1</text>
- <text font-size='9' x='647.473525' y='15' fill='#000' fill-opacity='.4' textLength='28.176989999999996'>deploy</text>
- <text font-size='9' x='646.973525' y='14' fill='#fff' textLength='28.176989999999996'>deploy</text>
- <text font-size='9' x='675.657335' y='15' fill='#000' fill-opacity='.4' textLength='16.190630000000002'>test</text>
- <text font-size='9' x='675.157335' y='14' fill='#fff' textLength='16.190630000000002'>test</text>
- </g>
-</svg>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json
deleted file mode 100644
index 978258a31a6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/root.json
+++ /dev/null
@@ -1,380 +0,0 @@
-{
- "versions": [
- {
- "version": "5",
- "confidence": "high",
- "commit": "badc0ffee",
- "date": 0,
- "controllerVersion": false,
- "systemVersion": false,
- "configServers": [ ],
- "failingApplications": [ ],
- "productionApplications": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "default",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1",
- "upgradePolicy": "default",
- "productionJobs": 1,
- "productionSuccesses": 1
- },
- {
- "tenant": "tenant2",
- "application": "application2",
- "instance": "i2",
- "url": "http://localhost:8080/application/v4/tenant/tenant2/application/application2",
- "upgradePolicy": "default",
- "productionJobs": 1,
- "productionSuccesses": 1
- }
- ],
- "deployingApplications": [ ],
- "applications": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "default",
- "upgrading": false,
- "pinned": false,
- "platformPinned": false,
- "revisionPinned": false,
- "upgradePolicy": "default",
- "compileVersion": "6.1.0",
- "jobs": [
- {
- "name": "system-test"
- },
- {
- "name": "staging-test",
- "coolingDownUntil": 1600022201500
- },
- {
- "name": "production-aws-us-east-1a"
- }
- ],
- "allRuns": {
- "production-aws-us-east-1a": {
- "success": {
- "number": 1,
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "enclave": {
- "cloudAccount": "aws:123456789012"
- }
- }
- }
- },
- "upgradeRuns": {
- "production-aws-us-east-1a": {
- "success": {
- "number": 1,
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success",
- "enclave": {
- "cloudAccount": "aws:123456789012"
- }
- }
- }
- }
- },
- {
- "tenant": "tenant2",
- "application": "application2",
- "instance": "i2",
- "upgrading": false,
- "pinned": false,
- "platformPinned": false,
- "revisionPinned": false,
- "upgradePolicy": "default",
- "compileVersion": "6.1.0",
- "jobs": [
- {
- "name": "production-us-west-1"
- }
- ],
- "allRuns": {
- "production-us-west-1": {
- "success": {
- "number": 1,
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success"
- }
- }
- },
- "upgradeRuns": {
- "production-us-west-1": {
- "success": {
- "number": 1,
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success"
- }
- }
- }
- }
- ]
- },
- {
- "version": "5.1",
- "confidence": "normal",
- "commit": "badc0ffee",
- "date": 0,
- "controllerVersion": true,
- "systemVersion": true,
- "configServers": [
- {
- "hostname": "config1.test"
- },
- {
- "hostname": "config2.test"
- }
- ],
- "failingApplications": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "default",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1",
- "upgradePolicy": "default",
- "failing": "system-test",
- "status": "error"
- },
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "default",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1",
- "upgradePolicy": "default",
- "failing": "staging-test",
- "status": "installationFailed"
- }
- ],
- "productionApplications": [
- {
- "tenant": "tenant2",
- "application": "application2",
- "instance": "i1",
- "url": "http://localhost:8080/application/v4/tenant/tenant2/application/application2",
- "upgradePolicy": "default",
- "productionJobs": 1,
- "productionSuccesses": 1
- }
- ],
- "deployingApplications": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "default",
- "url": "http://localhost:8080/application/v4/tenant/tenant1/application/application1",
- "upgradePolicy": "default",
- "running": "system-test"
- },
- {
- "tenant": "tenant2",
- "application": "application2",
- "instance": "i2",
- "url": "http://localhost:8080/application/v4/tenant/tenant2/application/application2",
- "upgradePolicy": "default",
- "running": "production-us-west-1"
- }
- ],
- "applications": [
- {
- "tenant": "tenant1",
- "application": "application1",
- "instance": "default",
- "upgrading": true,
- "pinned": false,
- "platformPinned": false,
- "revisionPinned": false,
- "upgradePolicy": "default",
- "compileVersion": "6.1.0",
- "jobs": [
- {
- "name": "system-test",
- "pending": "application"
- },
- {
- "name": "staging-test",
- "coolingDownUntil": 1600022201500,
- "pending": "platform"
- },
- {
- "name": "production-aws-us-east-1a",
- "pending": "platform"
- }
- ],
- "allRuns": {
- "system-test": {
- "failing": {
- "number": 2,
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "error"
- },
- "running": {
- "number": 3,
- "start": 1600000000000,
- "status": "running"
- }
- },
- "staging-test": {
- "failing": {
- "number": 3,
- "start": 1600000000000,
- "end": 1600014401000,
- "status": "installationFailed"
- }
- }
- },
- "upgradeRuns": {
- "system-test": {
- "failing": {
- "number": 2,
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "error"
- },
- "running": {
- "number": 3,
- "start": 1600000000000,
- "status": "running"
- }
- },
- "staging-test": {
- "failing": {
- "number": 3,
- "start": 1600000000000,
- "end": 1600014401000,
- "status": "installationFailed"
- }
- }
- }
- },
- {
- "tenant": "tenant2",
- "application": "application2",
- "instance": "i1",
- "upgrading": false,
- "pinned": false,
- "platformPinned": false,
- "revisionPinned": false,
- "upgradePolicy": "default",
- "compileVersion": "6.1.0",
- "jobs": [
- {
- "name": "system-test",
- "pending": "application"
- },
- {
- "name": "staging-test",
- "pending": "application"
- },
- {
- "name": "production-us-west-1",
- "pending": "application"
- }
- ],
- "allRuns": {
- "system-test": {
- "failing": {
- "number": 3,
- "start": 1600014401000,
- "end": 1600014401000,
- "status": "error"
- }
- },
- "staging-test": {
- "running": {
- "number": 3,
- "start": 1600014401000,
- "status": "running"
- }
- },
- "production-us-west-1": {
- "success": {
- "number": 2,
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success"
- }
- }
- },
- "upgradeRuns": {
- "system-test": { },
- "staging-test": { },
- "production-us-west-1": {
- "success": {
- "number": 2,
- "start": 1600000000000,
- "end": 1600000000000,
- "status": "success"
- }
- }
- }
- },
- {
- "tenant": "tenant2",
- "application": "application2",
- "instance": "i2",
- "upgrading": true,
- "pinned": false,
- "platformPinned": false,
- "revisionPinned": false,
- "upgradePolicy": "default",
- "compileVersion": "6.1.0",
- "jobs": [
- {
- "name": "production-us-west-1",
- "pending": "platform"
- }
- ],
- "allRuns": {
- "production-us-west-1": {
- "running": {
- "number": 2,
- "start": 1600014401000,
- "status": "running"
- }
- }
- },
- "upgradeRuns": {
- "production-us-west-1": {
- "running": {
- "number": 2,
- "start": 1600014401000,
- "status": "running"
- }
- }
- }
- }
- ]
- }
- ],
- "jobs": [
- "system-test",
- "staging-test",
- "production-ap-northeast-1",
- "test-ap-northeast-1",
- "production-ap-northeast-2",
- "test-ap-northeast-2",
- "production-ap-southeast-1",
- "test-ap-southeast-1",
- "production-aws-us-east-1a",
- "test-aws-us-east-1a",
- "production-aws-us-east-1b",
- "test-aws-us-east-1b",
- "production-eu-west-1",
- "test-eu-west-1",
- "production-us-central-1",
- "test-us-central-1",
- "production-us-east-3",
- "test-us-east-3",
- "production-us-west-1",
- "test-us-west-1"
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/running-test.svg b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/running-test.svg
deleted file mode 100644
index 9463c01e8ad..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/running-test.svg
+++ /dev/null
@@ -1,93 +0,0 @@
-<svg xmlns='http://www.w3.org/2000/svg' width='300.38627894289516' height='20' role='img' aria-label='Deployment Status'>
- <title>Deployment Status</title>
- <linearGradient id='light' x2='0' y2='100%'>
- <stop offset='0' stop-color='#fff' stop-opacity='.5'/>
- <stop offset='.1' stop-color='#fff' stop-opacity='.15'/>
- <stop offset='.9' stop-color='#000' stop-opacity='.15'/>
- <stop offset='1' stop-color='#000' stop-opacity='.5'/>
- </linearGradient>
- <linearGradient id='left-light' x2='100%' y2='0'>
- <stop offset='0' stop-color='#fff' stop-opacity='.3'/>
- <stop offset='.5' stop-color='#fff' stop-opacity='.1'/>
- <stop offset='1' stop-color='#fff' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='right-shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.0'/>
- <stop offset='.5' stop-color='#000' stop-opacity='.1'/>
- <stop offset='1' stop-color='#000' stop-opacity='.3'/>
- </linearGradient>
- <linearGradient id='shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#222' stop-opacity='.3'/>
- <stop offset='.625' stop-color='#555' stop-opacity='.3'/>
- <stop offset='.9' stop-color='#555' stop-opacity='.05'/>
- <stop offset='1' stop-color='#555' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='shade' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.20'/>
- <stop offset='0.05' stop-color='#000' stop-opacity='.10'/>
- <stop offset='1' stop-color='#000' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bf103c' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-warning' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bd890b' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#00f844' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <clipPath id='rounded'>
- <rect width='300.38627894289516' height='20' rx='3' fill='#fff'/>
- </clipPath>
- <g clip-path='url(#rounded)'>
- <rect x='297.7936599677133' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='267.7546449838567' rx='3' width='36.03901498385664' height='20' fill='#00f844'/>
- <rect x='267.7546449838567' rx='3' width='36.03901498385664' height='20' fill='url(#shade)'/>
- <rect x='271.5979829411765' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='237.71563000000003' rx='3' width='39.88235294117647' height='20' fill='#00f844'/>
- <rect x='237.71563000000003' rx='3' width='39.88235294117647' height='20' fill='url(#shade)'/>
- <rect x='237.71563000000003' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='163.18729000000002' rx='3' width='80.52834000000001' height='20' fill='url(#run-on-warning)'/>
- <rect x='163.18729000000002' rx='3' width='80.52834000000001' height='20' fill='url(#shade)'/>
- <rect width='169.18729000000002' height='20' fill='#404040'/>
- <rect x='-6.0' rx='3' width='175.18729000000002' height='20' fill='url(#shade)'/>
- <rect width='2' height='20' fill='url(#left-light)'/>
- <rect x='298.38627894289516' width='2' height='20' fill='url(#right-shadow)'/>
- <rect width='300.38627894289516' height='20' fill='url(#light)'/>
- </g>
- <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='11'>
- <svg x='6.5' y='3.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <svg x='6.0' y='2.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'>
- <stop offset='0.01' stop-color='#c6783e'/>
- <stop offset='0.54' stop-color='#ff9750'/>
- </linearGradient>
- <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'>
- <stop offset='0' stop-color='#005a8e'/>
- <stop offset='0.54' stop-color='#1a7db6'/>
- </linearGradient>
- <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <text font-size='11' x='96.09364500000001' y='15' fill='#000' fill-opacity='.4' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='95.59364500000001' y='14' fill='#fff' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='206.95146000000003' y='15' fill='#000' fill-opacity='.4' textLength='62.52834000000001'>system-test</text>
- <text font-size='11' x='206.45146000000003' y='14' fill='#fff' textLength='62.52834000000001'>system-test</text>
- </g>
-</svg>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/single-done.svg b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/single-done.svg
deleted file mode 100644
index 0bdaa3f30ad..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/single-done.svg
+++ /dev/null
@@ -1,87 +0,0 @@
-<svg xmlns='http://www.w3.org/2000/svg' width='294.82606' height='20' role='img' aria-label='Deployment Status'>
- <title>Deployment Status</title>
- <linearGradient id='light' x2='0' y2='100%'>
- <stop offset='0' stop-color='#fff' stop-opacity='.5'/>
- <stop offset='.1' stop-color='#fff' stop-opacity='.15'/>
- <stop offset='.9' stop-color='#000' stop-opacity='.15'/>
- <stop offset='1' stop-color='#000' stop-opacity='.5'/>
- </linearGradient>
- <linearGradient id='left-light' x2='100%' y2='0'>
- <stop offset='0' stop-color='#fff' stop-opacity='.3'/>
- <stop offset='.5' stop-color='#fff' stop-opacity='.1'/>
- <stop offset='1' stop-color='#fff' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='right-shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.0'/>
- <stop offset='.5' stop-color='#000' stop-opacity='.1'/>
- <stop offset='1' stop-color='#000' stop-opacity='.3'/>
- </linearGradient>
- <linearGradient id='shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#222' stop-opacity='.3'/>
- <stop offset='.625' stop-color='#555' stop-opacity='.3'/>
- <stop offset='.9' stop-color='#555' stop-opacity='.05'/>
- <stop offset='1' stop-color='#555' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='shade' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.20'/>
- <stop offset='0.05' stop-color='#000' stop-opacity='.10'/>
- <stop offset='1' stop-color='#000' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bf103c' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-warning' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bd890b' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#00f844' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <clipPath id='rounded'>
- <rect width='294.82606' height='20' rx='3' fill='#fff'/>
- </clipPath>
- <g clip-path='url(#rounded)'>
- <rect x='288.82606' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='163.18729000000002' rx='3' width='131.63876999999997' height='20' fill='#00f844'/>
- <rect x='163.18729000000002' rx='3' width='131.63876999999997' height='20' fill='url(#shade)'/>
- <rect width='169.18729000000002' height='20' fill='#404040'/>
- <rect x='-6.0' rx='3' width='175.18729000000002' height='20' fill='url(#shade)'/>
- <rect width='2' height='20' fill='url(#left-light)'/>
- <rect x='292.82606' width='2' height='20' fill='url(#right-shadow)'/>
- <rect width='294.82606' height='20' fill='url(#light)'/>
- </g>
- <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='11'>
- <svg x='6.5' y='3.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <svg x='6.0' y='2.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'>
- <stop offset='0.01' stop-color='#c6783e'/>
- <stop offset='0.54' stop-color='#ff9750'/>
- </linearGradient>
- <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'>
- <stop offset='0' stop-color='#005a8e'/>
- <stop offset='0.54' stop-color='#1a7db6'/>
- </linearGradient>
- <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <text font-size='11' x='96.09364500000001' y='15' fill='#000' fill-opacity='.4' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='95.59364500000001' y='14' fill='#fff' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='232.506675' y='15' fill='#000' fill-opacity='.4' textLength='113.63876999999998'>production-us-west-1</text>
- <text font-size='11' x='232.006675' y='14' fill='#fff' textLength='113.63876999999998'>production-us-west-1</text>
- </g>
-</svg>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/single-running.svg b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/single-running.svg
deleted file mode 100644
index 1a38228e75d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/deployment/responses/single-running.svg
+++ /dev/null
@@ -1,87 +0,0 @@
-<svg xmlns='http://www.w3.org/2000/svg' width='294.82606' height='20' role='img' aria-label='Deployment Status'>
- <title>Deployment Status</title>
- <linearGradient id='light' x2='0' y2='100%'>
- <stop offset='0' stop-color='#fff' stop-opacity='.5'/>
- <stop offset='.1' stop-color='#fff' stop-opacity='.15'/>
- <stop offset='.9' stop-color='#000' stop-opacity='.15'/>
- <stop offset='1' stop-color='#000' stop-opacity='.5'/>
- </linearGradient>
- <linearGradient id='left-light' x2='100%' y2='0'>
- <stop offset='0' stop-color='#fff' stop-opacity='.3'/>
- <stop offset='.5' stop-color='#fff' stop-opacity='.1'/>
- <stop offset='1' stop-color='#fff' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='right-shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.0'/>
- <stop offset='.5' stop-color='#000' stop-opacity='.1'/>
- <stop offset='1' stop-color='#000' stop-opacity='.3'/>
- </linearGradient>
- <linearGradient id='shadow' x2='100%' y2='0'>
- <stop offset='0' stop-color='#222' stop-opacity='.3'/>
- <stop offset='.625' stop-color='#555' stop-opacity='.3'/>
- <stop offset='.9' stop-color='#555' stop-opacity='.05'/>
- <stop offset='1' stop-color='#555' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='shade' x2='100%' y2='0'>
- <stop offset='0' stop-color='#000' stop-opacity='.20'/>
- <stop offset='0.05' stop-color='#000' stop-opacity='.10'/>
- <stop offset='1' stop-color='#000' stop-opacity='.0'/>
- </linearGradient>
- <linearGradient id='run-on-failure' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bf103c' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-warning' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#bd890b' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <linearGradient id='run-on-success' x1='40%' x2='80%' y2='0%'>
- <stop offset='0' stop-color='#ab83ff' />
- <stop offset='1' stop-color='#00f844' />
- <animate attributeName='x1' values='-110%;150%;20%;-110%' dur='6s' repeatCount='indefinite' />
- <animate attributeName='x2' values='-10%;250%;120%;-10%' dur='6s' repeatCount='indefinite' />
- </linearGradient>
- <clipPath id='rounded'>
- <rect width='294.82606' height='20' rx='3' fill='#fff'/>
- </clipPath>
- <g clip-path='url(#rounded)'>
- <rect x='288.82606' rx='3' width='8' height='20' fill='url(#shadow)'/>
- <rect x='163.18729000000002' rx='3' width='131.63876999999997' height='20' fill='url(#run-on-failure)'/>
- <rect x='163.18729000000002' rx='3' width='131.63876999999997' height='20' fill='url(#shade)'/>
- <rect width='169.18729000000002' height='20' fill='#404040'/>
- <rect x='-6.0' rx='3' width='175.18729000000002' height='20' fill='url(#shade)'/>
- <rect width='2' height='20' fill='url(#left-light)'/>
- <rect x='292.82606' width='2' height='20' fill='url(#right-shadow)'/>
- <rect width='294.82606' height='20' fill='url(#light)'/>
- </g>
- <g fill='#fff' text-anchor='middle' font-family='Verdana,Geneva,DejaVu Sans,sans-serif' text-rendering='geometricPrecision' font-size='11'>
- <svg x='6.5' y='3.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='#402a14' fill-opacity='0.5' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='#061a29' fill-opacity='0.5' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <svg x='6.0' y='2.0' width='16.0' height='16.0' viewBox='0 0 150 150'>
- <linearGradient id='yellow-shaded' x1='91.17' y1='44.83' x2='136.24' y2='73.4' gradientUnits='userSpaceOnUse'>
- <stop offset='0.01' stop-color='#c6783e'/>
- <stop offset='0.54' stop-color='#ff9750'/>
- </linearGradient>
- <linearGradient id='blue-shaded' x1='60.71' y1='104.56' x2='-15.54' y2='63' gradientUnits='userSpaceOnUse'>
- <stop offset='0' stop-color='#005a8e'/>
- <stop offset='0.54' stop-color='#1a7db6'/>
- </linearGradient>
- <polygon fill='#ff9d4b' points='84.84 10 34.1 44.46 34.1 103.78 84.84 68.02 135.57 103.78 135.57 44.46 84.84 10'/>
- <polygon fill='url(#yellow-shaded)' points='84.84 68.02 84.84 10 135.57 44.46 135.57 103.78 84.84 68.02'/>
- <polygon fill='#1a7db6' points='65.07 81.99 14.34 46.22 14.34 105.54 65.07 140 115.81 105.54 115.81 46.22 65.07 81.99'/>
- <polygon fill='url(#blue-shaded)' points='65.07 81.99 65.07 140 14.34 105.54 14.34 46.22 65.07 81.99'/>
- </svg>
- <text font-size='11' x='96.09364500000001' y='15' fill='#000' fill-opacity='.4' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='95.59364500000001' y='14' fill='#fff' textLength='135.18729000000002'>tenant.application.default</text>
- <text font-size='11' x='232.506675' y='15' fill='#000' fill-opacity='.4' textLength='113.63876999999998'>production-us-west-1</text>
- <text font-size='11' x='232.006675' y='14' fill='#fff' textLength='113.63876999999998'>production-us-west-1</text>
- </g>
-</svg>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java
deleted file mode 100644
index fe0a3551860..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/AthenzRoleFilterTest.java
+++ /dev/null
@@ -1,136 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.filter;
-
-import com.yahoo.config.provision.ApplicationName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.AthenzService;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ApplicationId;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ScrewdriverId;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.ApplicationAction;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzClientFactoryMock;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.athenz.HostedAthenzIdentities;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author jonmv
- */
-public class AthenzRoleFilterTest {
-
- private static final AthenzPrincipal USER = new AthenzPrincipal(new AthenzUser("john"));
- private static final AthenzPrincipal HOSTED_OPERATOR = new AthenzPrincipal(new AthenzUser("hosted-operator"));
- private static final AthenzDomain TENANT_DOMAIN = new AthenzDomain("tenantdomain");
- private static final AthenzDomain TENANT_DOMAIN2 = new AthenzDomain("tenantdomain2");
- private static final AthenzPrincipal TENANT_ADMIN = new AthenzPrincipal(new AthenzService(TENANT_DOMAIN, "adminservice"));
- private static final AthenzPrincipal TENANT_PIPELINE = new AthenzPrincipal(HostedAthenzIdentities.from(new ScrewdriverId("12345")));
- private static final AthenzPrincipal TENANT_ADMIN_AND_PIPELINE = new AthenzPrincipal(HostedAthenzIdentities.from(new ScrewdriverId("56789")));
- private static final TenantName TENANT = TenantName.from("mytenant");
- private static final TenantName TENANT2 = TenantName.from("othertenant");
- private static final ApplicationName APPLICATION = ApplicationName.from("myapp");
- private static final URI NO_CONTEXT_PATH = URI.create("/application/v4/");
- private static final URI TENANT_CONTEXT_PATH = URI.create("/application/v4/tenant/mytenant/");
- private static final URI APPLICATION_CONTEXT_PATH = URI.create("/application/v4/tenant/mytenant/application/myapp/");
- private static final URI TENANT2_CONTEXT_PATH = URI.create("/application/v4/tenant/othertenant/");
- private static final URI APPLICATION2_CONTEXT_PATH = URI.create("/application/v4/tenant/othertenant/application/myapp/");
- private static final URI INSTANCE_CONTEXT_PATH = URI.create("/application/v4/tenant/mytenant/application/myapp/instance/john");
- private static final URI INSTANCE2_CONTEXT_PATH = URI.create("/application/v4/tenant/mytenant/application/myapp/instance/jane");
-
- private AthenzRoleFilter filter;
-
- @BeforeEach
- public void setup() {
- ControllerTester tester = new ControllerTester();
- filter = new AthenzRoleFilter(new AthenzClientFactoryMock(tester.athenzDb()),
- tester.controller());
-
- tester.athenzDb().hostedOperators.add(HOSTED_OPERATOR.getIdentity());
- tester.createTenant(TENANT.value(), TENANT_DOMAIN.getName(), null);
- tester.createApplication(TENANT.value(), APPLICATION.value(), "default");
- AthenzDbMock.Domain tenantDomain = tester.athenzDb().domains.get(TENANT_DOMAIN);
- tenantDomain.admins.add(TENANT_ADMIN.getIdentity());
- tenantDomain.admins.add(TENANT_ADMIN_AND_PIPELINE.getIdentity());
- tenantDomain.applications.get(new ApplicationId(APPLICATION.value())).addRoleMember(ApplicationAction.deploy, TENANT_PIPELINE.getIdentity());
- tenantDomain.applications.get(new ApplicationId(APPLICATION.value())).addRoleMember(ApplicationAction.deploy, TENANT_ADMIN_AND_PIPELINE.getIdentity());
- tester.createTenant(TENANT2.value(), TENANT_DOMAIN2.getName(), null);
- tester.createApplication(TENANT2.value(), APPLICATION.value(), "default");
- }
-
- @Test
- void testTranslations() throws Exception {
-
- // Hosted operators are always members of the hostedOperator role.
- assertEquals(Set.of(Role.hostedOperator(), Role.systemFlagsDeployer(), Role.systemFlagsDryrunner(), Role.paymentProcessor(), Role.hostedAccountant(), Role.hostedSupporter()),
- filter.roles(HOSTED_OPERATOR, NO_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.hostedOperator(), Role.systemFlagsDeployer(), Role.systemFlagsDryrunner(), Role.paymentProcessor(), Role.hostedAccountant(), Role.hostedSupporter()),
- filter.roles(HOSTED_OPERATOR, TENANT_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.hostedOperator(), Role.systemFlagsDeployer(), Role.systemFlagsDryrunner(), Role.paymentProcessor(), Role.hostedAccountant(), Role.hostedSupporter()),
- filter.roles(HOSTED_OPERATOR, APPLICATION_CONTEXT_PATH));
-
- // Tenant admins are members of the athenzTenantAdmin role within their tenant subtree.
- assertEquals(Set.of(Role.everyone()),
- filter.roles(TENANT_PIPELINE, NO_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.athenzTenantAdmin(TENANT)),
- filter.roles(TENANT_ADMIN, TENANT_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.athenzTenantAdmin(TENANT)),
- filter.roles(TENANT_ADMIN, APPLICATION_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.athenzTenantAdmin(TENANT)),
- filter.roles(TENANT_ADMIN, TENANT2_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.athenzTenantAdmin(TENANT)),
- filter.roles(TENANT_ADMIN, APPLICATION2_CONTEXT_PATH));
-
- // Build services are members of the buildService role within their application subtree.
- assertEquals(Set.of(Role.everyone()),
- filter.roles(TENANT_PIPELINE, NO_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.everyone()),
- filter.roles(TENANT_PIPELINE, TENANT_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.buildService(TENANT, APPLICATION)),
- filter.roles(TENANT_PIPELINE, APPLICATION_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.everyone()),
- filter.roles(TENANT_PIPELINE, APPLICATION2_CONTEXT_PATH));
-
- // Principals member of both tenantPipeline and tenantAdmin roles get correct roles
- assertEquals(Set.of(Role.athenzTenantAdmin(TENANT)),
- filter.roles(TENANT_ADMIN_AND_PIPELINE, TENANT_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.athenzTenantAdmin(TENANT), Role.buildService(TENANT, APPLICATION)),
- filter.roles(TENANT_ADMIN_AND_PIPELINE, APPLICATION_CONTEXT_PATH));
-
- // Users have nothing special under their instance
- assertEquals(Set.of(Role.everyone()),
- filter.roles(USER, INSTANCE_CONTEXT_PATH));
-
- // Unprivileged users are just members of the everyone role.
- assertEquals(Set.of(Role.everyone()),
- filter.roles(USER, NO_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.everyone()),
- filter.roles(USER, TENANT_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.everyone()),
- filter.roles(USER, APPLICATION_CONTEXT_PATH));
-
- assertEquals(Set.of(Role.everyone()),
- filter.roles(USER, INSTANCE2_CONTEXT_PATH));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java
deleted file mode 100644
index 0b993fc70a3..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/ControllerAuthorizationFilterTest.java
+++ /dev/null
@@ -1,137 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.filter;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.jdisc.http.HttpRequest.Method;
-import com.yahoo.jdisc.http.filter.DiscFilterRequest;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.Optional;
-import java.util.Set;
-
-import static com.yahoo.container.jdisc.RequestHandlerTestDriver.MockResponseHandler;
-import static com.yahoo.jdisc.http.HttpResponse.Status.FORBIDDEN;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author bjorncs
- * @author jonmv
- */
-public class ControllerAuthorizationFilterTest {
-
- private static final ObjectMapper mapper = new ObjectMapper();
-
- @Test
- void operator() {
- ControllerTester tester = new ControllerTester();
- SecurityContext securityContext = new SecurityContext(() -> "operator", Set.of(Role.hostedOperator()));
- ControllerAuthorizationFilter filter = createFilter(tester);
-
- assertIsAllowed(invokeFilter(filter, createRequest(Method.POST, "/zone/v2/path", securityContext)));
- assertIsAllowed(invokeFilter(filter, createRequest(Method.PUT, "/application/v4/user", securityContext)));
- assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", securityContext)));
- }
-
- @Test
- void supporter() {
- ControllerTester tester = new ControllerTester();
- SecurityContext securityContext = new SecurityContext(() -> "operator", Set.of(Role.hostedSupporter()));
- ControllerAuthorizationFilter filter = createFilter(tester);
-
- assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/zone/v2/path", securityContext)));
- assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", securityContext)));
- }
-
- @Test
- void unprivileged() {
- ControllerTester tester = new ControllerTester();
- SecurityContext securityContext = new SecurityContext(() -> "user", Set.of(Role.everyone()));
- ControllerAuthorizationFilter filter = createFilter(tester);
-
- assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/zone/v2/path", securityContext)));
- assertIsAllowed(invokeFilter(filter, createRequest(Method.PUT, "/application/v4/user", securityContext)));
- assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", securityContext)));
- }
-
- @Test
- void unprivilegedInPublic() {
- ControllerTester tester = new ControllerTester(SystemName.Public);
- SecurityContext securityContext = new SecurityContext(() -> "user", Set.of(Role.everyone()));
-
- ControllerAuthorizationFilter filter = createFilter(tester);
- assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/zone/v2/path", securityContext)));
- assertIsForbidden(invokeFilter(filter, createRequest(Method.PUT, "/application/v4/user", securityContext)));
- assertIsAllowed(invokeFilter(filter, createRequest(Method.GET, "/zone/v1/path", securityContext)));
- }
-
- @Test
- void hostedDeveloper() {
- ControllerTester tester = new ControllerTester();
- TenantName tenantName = TenantName.defaultName();
- SecurityContext securityContext = new SecurityContext(() -> "user", Set.of(Role.hostedDeveloper(tenantName)));
-
- ControllerAuthorizationFilter filter = createFilter(tester);
- assertIsAllowed(invokeFilter(filter, createRequest(Method.POST, "/application/v4/tenant/" + tenantName.value() + "/application/app/instance/default/environment/dev/region/region/deploy", securityContext)));
- assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/application/v4/tenant/" + tenantName.value() + "/application/app/instance/default/environment/prod/region/region/deploy", securityContext)));
- assertIsForbidden(invokeFilter(filter, createRequest(Method.POST, "/application/v4/tenant/" + tenantName.value() + "/application/app/submit", securityContext)));
- }
-
- private static void assertIsAllowed(Optional<AuthorizationResponse> response) {
- assertFalse(response.isPresent(),
- "Expected no response from filter, but got \"" +
- response.map(r -> r.message + "\" (" + r.statusCode + ")").orElse(""));
- }
-
- private static void assertIsForbidden(Optional<AuthorizationResponse> response) {
- assertTrue(response.isPresent(), "Expected a response from filter");
- assertEquals(FORBIDDEN, response.get().statusCode, "Invalid status code");
- }
-
- private static ControllerAuthorizationFilter createFilter(ControllerTester tester) {
- return new ControllerAuthorizationFilter(tester.controller());
- }
-
- private static Optional<AuthorizationResponse> invokeFilter(ControllerAuthorizationFilter filter,
- DiscFilterRequest request) {
- MockResponseHandler responseHandlerMock = new MockResponseHandler();
- filter.filter(request, responseHandlerMock);
- return Optional.ofNullable(responseHandlerMock.getResponse())
- .map(response -> new AuthorizationResponse(response.getStatus(), getErrorMessage(responseHandlerMock)));
- }
-
- private static DiscFilterRequest createRequest(Method method, String path, SecurityContext securityContext) {
- Request request = new Request(path, new byte[0], Request.Method.valueOf(method.name()), securityContext.principal());
- request.getAttributes().put(SecurityContext.ATTRIBUTE_NAME, securityContext);
- return new ApplicationRequestToDiscFilterRequestWrapper(request);
- }
-
- private static String getErrorMessage(MockResponseHandler responseHandler) {
- try {
- return mapper.readTree(responseHandler.readAll()).get("message").asText();
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
- private static class AuthorizationResponse {
- final int statusCode;
- final String message;
-
- AuthorizationResponse(int statusCode, String message) {
- this.statusCode = statusCode;
- this.message = message;
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java
deleted file mode 100644
index 8abf32c45f9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/LastLoginUpdateFilterTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.filter;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.util.Set;
-
-import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.administrator;
-import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.developer;
-import static com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo.UserLevel.user;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class LastLoginUpdateFilterTest {
-
- private static final TenantName tenant1 = TenantName.from("tenant1");
- private static final TenantName tenant2 = TenantName.from("tenant2");
-
- private final ControllerTester tester = new ControllerTester();
- private final LastLoginUpdateFilter filter = new LastLoginUpdateFilter(tester.controller());
-
- @Test
- void updateLastLoginTimeTest() {
- tester.createTenant(tenant1.value());
- tester.createTenant(tenant2.value());
-
- request(123, Role.developer(tenant1), Role.reader(tenant1), Role.athenzTenantAdmin(tenant2));
- assertLastLoginBy(tenant1, 123L, 123L, null);
- assertLastLoginBy(tenant2, 123L, 123L, 123L);
-
- request(321, Role.administrator(tenant1), Role.reader(tenant1));
- assertLastLoginBy(tenant1, 321L, 123L, 321L);
- assertLastLoginBy(tenant2, 123L, 123L, 123L);
- }
-
- private void assertLastLoginBy(TenantName tenantName, Long lastUserLoginAt, Long lastDeveloperLoginAt, Long lastAdministratorLoginAt) {
- LastLoginInfo loginInfo = tester.controller().tenants().require(tenantName).lastLoginInfo();
- assertEquals(lastUserLoginAt, loginInfo.get(user).map(Instant::toEpochMilli).orElse(null));
- assertEquals(lastDeveloperLoginAt, loginInfo.get(developer).map(Instant::toEpochMilli).orElse(null));
- assertEquals(lastAdministratorLoginAt, loginInfo.get(administrator).map(Instant::toEpochMilli).orElse(null));
- }
-
- private void request(long issuedAt, Role... roles) {
- SecurityContext context = new SecurityContext(() -> "bob", Set.of(roles), Instant.ofEpochMilli(issuedAt));
- Request request = new Request("/", new byte[0], Request.Method.GET, context.principal());
- request.getAttributes().put(SecurityContext.ATTRIBUTE_NAME, context);
- filter.filter(new ApplicationRequestToDiscFilterRequestWrapper(request));
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
deleted file mode 100644
index 9477e71af33..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/filter/SignatureFilterTest.java
+++ /dev/null
@@ -1,148 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.filter;
-
-import ai.vespa.hosted.api.Method;
-import ai.vespa.hosted.api.RequestSigner;
-import com.google.common.collect.ImmutableBiMap;
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.jdisc.http.filter.DiscFilterRequest;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.vespa.hosted.controller.Application;
-import com.yahoo.vespa.hosted.controller.ApplicationController;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.restapi.ApplicationRequestToDiscFilterRequestWrapper;
-import com.yahoo.vespa.hosted.controller.tenant.ArchiveAccess;
-import com.yahoo.vespa.hosted.controller.tenant.CloudTenant;
-import com.yahoo.vespa.hosted.controller.tenant.LastLoginInfo;
-import com.yahoo.vespa.hosted.controller.tenant.TenantInfo;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.http.HttpRequest;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.time.Instant;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class SignatureFilterTest {
-
- private static final PublicKey publicKey = KeyUtils.fromPemEncodedPublicKey("-----BEGIN PUBLIC KEY-----\n" +
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" +
- "z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
- "-----END PUBLIC KEY-----\n");
-
- private static final PublicKey otherPublicKey = KeyUtils.fromPemEncodedPublicKey("-----BEGIN PUBLIC KEY-----\n" +
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\n" +
- "pDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n" +
- "-----END PUBLIC KEY-----\n");
-
- private static final PrivateKey privateKey = KeyUtils.fromPemEncodedPrivateKey("-----BEGIN EC PRIVATE KEY-----\n" +
- "MHcCAQEEIJUmbIX8YFLHtpRgkwqDDE3igU9RG6JD9cYHWAZii9j7oAoGCCqGSM49\n" +
- "AwEHoUQDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9z/4jKSTHwbYR8wdsOSrJGVEU\n" +
- "PbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
- "-----END EC PRIVATE KEY-----\n");
-
- private static final TenantAndApplicationId appId = TenantAndApplicationId.from("my-tenant", "my-app");
- private static final ApplicationId id = appId.defaultInstance();
-
- private ControllerTester tester;
- private ApplicationController applications;
- private SignatureFilter filter;
- private RequestSigner signer;
-
- @BeforeEach
- public void setup() {
- tester = new ControllerTester();
- applications = tester.controller().applications();
- filter = new SignatureFilter(tester.controller());
- signer = new RequestSigner(privateKey, id.serializedForm(), tester.clock());
-
- tester.curator().writeTenant(CloudTenant.create(appId.tenant(), Instant.EPOCH, new SimplePrincipal("owner@my-tenant.my-app")));
- tester.curator().writeApplication(new Application(appId, tester.clock().instant()));
- }
-
- @Test
- void testFilter() {
- // Unsigned request gets no role.
- HttpRequest.Builder request = HttpRequest.newBuilder(URI.create("https://host:123/path/./..//..%2F?query=empty&%3F=%26"));
- byte[] emptyBody = new byte[0];
- verifySecurityContext(requestOf(request.copy().method("GET", HttpRequest.BodyPublishers.ofByteArray(emptyBody)).build(), emptyBody),
- null);
-
- // Signed request gets no role when no key is stored for the application.
- verifySecurityContext(requestOf(signer.signed(request.copy(), Method.GET, InputStream::nullInputStream), emptyBody),
- null);
-
- // Signed request gets no role when only non-matching keys are stored for the application.
- applications.lockApplicationOrThrow(appId, application -> applications.store(application.withDeployKey(otherPublicKey)));
- // Signed request gets no role when no key is stored for the application.
- verifySecurityContext(requestOf(signer.signed(request.copy(), Method.GET, InputStream::nullInputStream), emptyBody),
- null);
-
- // Signed request gets a headless role when a matching key is stored for the application.
- applications.lockApplicationOrThrow(appId, application -> applications.store(application.withDeployKey(publicKey)));
- verifySecurityContext(requestOf(signer.signed(request.copy(), Method.GET, InputStream::nullInputStream), emptyBody),
- new SecurityContext(new SimplePrincipal("headless@my-tenant.my-app"),
- Set.of(Role.reader(id.tenant()),
- Role.headless(id.tenant(), id.application())),
- tester.clock().instant()));
-
- // Signed POST request with X-Key header gets a headless role.
- byte[] hiBytes = new byte[]{0x48, 0x69};
- verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes),
- new SecurityContext(new SimplePrincipal("headless@my-tenant.my-app"),
- Set.of(Role.reader(id.tenant()),
- Role.headless(id.tenant(), id.application())),
- tester.clock().instant()));
-
- // Signed request gets a developer role when a matching developer key is stored for the tenant.
- tester.curator().writeTenant(new CloudTenant(appId.tenant(),
- Instant.EPOCH,
- LastLoginInfo.EMPTY,
- Optional.empty(),
- ImmutableBiMap.of(publicKey, new SimplePrincipal("user")),
- TenantInfo.empty(),
- List.of(),
- new ArchiveAccess(),
- Optional.empty(),
- Instant.EPOCH,
- List.of(),
- Optional.empty(),
- PlanId.from("none")));
- verifySecurityContext(requestOf(signer.signed(request.copy(), Method.POST, () -> new ByteArrayInputStream(hiBytes)), hiBytes),
- new SecurityContext(new SimplePrincipal("user"),
- Set.of(Role.reader(id.tenant()),
- Role.developer(id.tenant())),
- tester.clock().instant()));
-
- // Unsigned requests still get no roles.
- verifySecurityContext(requestOf(request.copy().method("GET", HttpRequest.BodyPublishers.ofByteArray(emptyBody)).build(), emptyBody),
- null);
- }
-
- private void verifySecurityContext(DiscFilterRequest request, SecurityContext securityContext) {
- assertTrue(filter.filter(request).isEmpty());
- assertEquals(securityContext, request.getAttribute(SecurityContext.ATTRIBUTE_NAME));
- }
-
- private static DiscFilterRequest requestOf(HttpRequest request, byte[] body) {
- Request converted = new Request(request.uri().toString(), body, Request.Method.valueOf(request.method()));
- converted.getHeaders().addAll(request.headers().map());
- return new ApplicationRequestToDiscFilterRequestWrapper(converted);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java
deleted file mode 100644
index 643ac82b223..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/AuditedFlagsApiTest.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.flags;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.hosted.controller.auditlog.AuditLog;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author mpolden
- */
-public class AuditedFlagsApiTest extends ControllerContainerTest {
-
- private static final String responses = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/flags/responses/";
- private static final AthenzIdentity operator = AthenzUser.fromUserId("operatorUser");
-
- private ContainerTester tester;
-
- @BeforeEach
- public void before() {
- addUserToHostedOperatorRole(operator);
- tester = new ContainerTester(container, responses);
- }
-
- @Test
- void test_audit_logging() {
- var body = "{\n" +
- " \"id\": \"id1\",\n" +
- " \"rules\": [\n" +
- " {\n" +
- " \"value\": true\n" +
- " }\n" +
- " ]\n" +
- "}";
- assertResponse(new Request("http://localhost:8080/flags/v1/data/id1?force=true", body, Request.Method.PUT),
- "", 200);
- var log = tester.controller().auditLogger().readLog();
- assertEquals(1, log.entries().size());
- var entry = log.entries().get(0);
- assertEquals(operator.getFullName(), entry.principal());
- assertEquals(AuditLog.Entry.Method.PUT, entry.method());
- assertEquals("/flags/v1/data/id1?force=true", entry.resource());
- assertEquals(body, log.entries().get(0).data().get());
- }
-
- private void assertResponse(Request request, String body, int statusCode) {
- addIdentityToRequest(request, operator);
- tester.assertResponse(request, body, statusCode);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java
deleted file mode 100644
index 87b4bf7e84c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/HorizonApiTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.horizon;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest;
-import org.junit.jupiter.api.Test;
-
-import java.util.Set;
-
-/**
- * @author olaa
- */
-public class HorizonApiTest extends ControllerContainerCloudTest {
-
- @Test
- void only_operators_and_flag_enabled_tenants_allowed() {
- ContainerTester tester = new ContainerTester(container, "");
- TenantName tenantName = TenantName.defaultName();
-
- tester.assertResponse(request("/horizon/v1/config/dashboard/topFolders")
- .roles(Set.of(Role.hostedOperator())),
- "", 200);
-
- tester.assertResponse(request("/horizon/v1/config/dashboard/topFolders")
- .roles(Set.of(Role.reader(tenantName))),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"No tenant with enabled metrics view\"}", 403);
-
- ((InMemoryFlagSource) tester.controller().flagSource())
- .withBooleanFlag(Flags.ENABLED_HORIZON_DASHBOARD.id(), true);
-
- tester.assertResponse(request("/horizon/v1/config/dashboard/topFolders")
- .roles(Set.of(Role.reader(tenantName))),
- "", 200);
- }
-
- @Override
- protected SystemName system() {
- return SystemName.PublicCd;
- }
-
- @Override
- protected String variablePartXml() {
- return " <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControlRequests'/>\n" +
- " <component id='com.yahoo.vespa.hosted.controller.security.CloudAccessControl'/>\n" +
-
- " <handler id=\"com.yahoo.vespa.hosted.controller.restapi.horizon.HorizonApiHandler\" bundle=\"controller-server\">\n" +
- " <binding>http://*/horizon/v1/*</binding>\n" +
- " </handler>\n" +
-
- " <http>\n" +
- " <server id='default' port='8080' />\n" +
- " <filtering>\n" +
- " <request-chain id='default'>\n" +
- " <filter id='com.yahoo.vespa.hosted.controller.restapi.filter.ControllerAuthorizationFilter'/>\n" +
- " <binding>http://*/*</binding>\n" +
- " </request-chain>\n" +
- " </filtering>\n" +
- " </http>\n";
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java
deleted file mode 100644
index 87d34874631..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/horizon/TsdbQueryRewriterTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.horizon;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.Set;
-
-import static com.yahoo.slime.SlimeUtils.jsonToSlimeOrThrow;
-import static com.yahoo.slime.SlimeUtils.toJsonBytes;
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author valerijf
- */
-public class TsdbQueryRewriterTest {
-
- @Test
- void rewrites_query() throws IOException {
- assertRewrite("filters-complex.json", "filters-complex.expected.json", Set.of(TenantName.from("tenant2")), false);
-
- assertRewrite("filter-in-execution-graph.json",
- "filter-in-execution-graph.expected.json",
- Set.of(TenantName.from("tenant2"), TenantName.from("tenant3")), false);
-
- assertRewrite("filter-in-execution-graph.json",
- "filter-in-execution-graph.expected.operator.json",
- Set.of(TenantName.from("tenant2"), TenantName.from("tenant3")), true);
-
- assertRewrite("no-filters.json",
- "no-filters.expected.json",
- Set.of(TenantName.from("tenant2"), TenantName.from("tenant3")), false);
-
- assertRewrite("filters-meta-query.json",
- "filters-meta-query.expected.json",
- Set.of(TenantName.from("tenant2"), TenantName.from("tenant3")), false);
- }
-
- private static void assertRewrite(String initialFilename, String expectedFilename, Set<TenantName> tenants, boolean operator) throws IOException {
- byte[] data = Files.readAllBytes(Paths.get("src/test/resources/horizon", initialFilename));
- data = TsdbQueryRewriter.rewrite(data, tenants, operator, SystemName.Public);
-
- String actualJson = new String(toJsonBytes(jsonToSlimeOrThrow(data).get(), false), UTF_8);
- String expectedJson = new String(toJsonBytes(jsonToSlimeOrThrow(Files.readAllBytes(Paths.get("src/test/resources/horizon", expectedFilename))).get(), false), UTF_8);
-
- assertEquals(expectedJson, actualJson);
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiTest.java
deleted file mode 100644
index 7a1b9979cfd..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/OsApiTest.java
+++ /dev/null
@@ -1,214 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.os;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.UpgradePolicy;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.OsRelease;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.integration.NodeRepositoryMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.maintenance.ControllerMaintainer;
-import com.yahoo.vespa.hosted.controller.maintenance.OsUpgrader;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import com.yahoo.vespa.hosted.controller.versions.OsVersionStatus;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-
-/**
- * @author mpolden
- */
-public class OsApiTest extends ControllerContainerTest {
-
- private static final String responses = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/";
- private static final AthenzIdentity operator = AthenzUser.fromUserId("operatorUser");
- private static final CloudName cloud1 = CloudName.from("cloud1");
- private static final CloudName cloud2 = CloudName.from("cloud2");
- private static final ZoneApi zone1 = ZoneApiMock.newBuilder().withId("prod.us-east-3").with(cloud1).build();
- private static final ZoneApi zone2 = ZoneApiMock.newBuilder().withId("prod.us-west-1").with(cloud1).build();
- private static final ZoneApi zone3 = ZoneApiMock.newBuilder().withId("prod.eu-west-1").with(cloud2).build();
-
- private ContainerTester tester;
- private List<OsUpgrader> osUpgraders;
-
- @Override
- protected SystemName system() {
- return SystemName.cd;
- }
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, responses);
- tester.serviceRegistry().clock().setInstant(Instant.ofEpochMilli(1234));
- addUserToHostedOperatorRole(operator);
- tester.serviceRegistry().zoneRegistry().setZones(zone1, zone2, zone3)
- .dynamicProvisioningIn(zone3)
- .setOsUpgradePolicy(cloud1, UpgradePolicy.builder().upgrade(zone1).upgrade(zone2).build())
- .setOsUpgradePolicy(cloud2, UpgradePolicy.builder().upgrade(zone3).build());
- tester.serviceRegistry().artifactRepository().addRelease(new OsRelease(Version.fromString("7.0"),
- OsRelease.Tag.latest,
- Instant.EPOCH));
- osUpgraders = List.of(
- new OsUpgrader(tester.controller(), Duration.ofDays(1),
- cloud1),
- new OsUpgrader(tester.controller(), Duration.ofDays(1),
- cloud2));
- }
-
- @Test
- void test_api() {
- // No versions available yet
- assertResponse(new Request("http://localhost:8080/os/v1/"), "{\"versions\":[]}", 200);
-
- // All nodes are initially on empty version
- upgradeAndUpdateStatus();
-
- // Upgrade OS to a different version in each cloud
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.5.2\", \"cloud\": \"cloud1\"}", Request.Method.PATCH),
- "{\"message\":\"Set target OS version for cloud 'cloud1' to 7.5.2\"}", 200);
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"8.2.1\", \"cloud\": \"cloud2\"}", Request.Method.PATCH),
- "{\"message\":\"Set target OS version for cloud 'cloud2' to 8.2.1\"}", 200);
-
- // Status is updated after some zones are upgraded
- upgradeAndUpdateStatus();
- completeUpgrade(zone1.getId());
- assertFile(new Request("http://localhost:8080/os/v1/"), "versions-partially-upgraded.json");
-
- // All zones are upgraded
- upgradeAndUpdateStatus();
- completeUpgrade(zone2.getId(), zone3.getId());
- assertFile(new Request("http://localhost:8080/os/v1/"), "versions-all-upgraded.json");
-
- // Downgrade with force is permitted
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"8.2.0\", \"cloud\": \"cloud2\", \"force\": true}", Request.Method.PATCH),
- "{\"message\":\"Set target OS version for cloud 'cloud2' to 8.2.0\"}", 200);
-
- // Clear target for a given cloud
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": null, \"cloud\": \"cloud1\"}", Request.Method.PATCH),
- "{\"message\":\"Cleared target OS version for cloud 'cloud1'\"}", 200);
-
- // Pin/unpin a version
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.5.2\", \"cloud\": \"cloud1\", \"pin\": true}", Request.Method.PATCH),
- "{\"message\":\"Set target OS version for cloud 'cloud1' to 7.5.2 (pinned)\"}", 200);
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.5.2\", \"cloud\": \"cloud1\", \"pin\": false}", Request.Method.PATCH),
- "{\"message\":\"Set target OS version for cloud 'cloud1' to 7.5.2\"}", 200);
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.5.2\", \"cloud\": \"cloud1\", \"pin\": true}", Request.Method.PATCH),
- "{\"message\":\"Set target OS version for cloud 'cloud1' to 7.5.2 (pinned)\"}", 200);
-
- // Certify an OS and Vespa version pair
- assertResponse(new Request("http://localhost:8080/os/v1/certify/cloud1/7.5.2", "8.200.37", Request.Method.POST),
- "{\"message\":\"Certified 7.5.2 in cloud cloud1 as compatible with Vespa version 8.200.37\"}", 200);
- assertResponse(new Request("http://localhost:8080/os/v1/certify/cloud2/7.5.2", "8.200.33", Request.Method.POST),
- "{\"message\":\"Certified 7.5.2 in cloud cloud2 as compatible with Vespa version 8.200.33\"}", 200);
- assertResponse(new Request("http://localhost:8080/os/v1/certify/cloud1/7.5.2", "8.200.42", Request.Method.POST),
- "{\"message\":\"7.5.2 is already certified in cloud cloud1 as compatible with Vespa version 8.200.37. Leaving certification unchanged\"}", 200);
- assertResponse(new Request("http://localhost:8080/os/v1/certify", "", Request.Method.GET),
- """
-[{"version":"7.5.2","cloud":"cloud1","vespaVersion":"8.200.37"},{"version":"7.5.2","cloud":"cloud2","vespaVersion":"8.200.33"}]""", 200);
- assertResponse(new Request("http://localhost:8080/os/v1/certify/cloud1/7.5.2", "", Request.Method.DELETE),
- "{\"message\":\"Removed certification of 7.5.2 in cloud cloud1\"}", 200);
-
- // Error: Missing fields
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.6\"}", Request.Method.PATCH),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Field 'cloud' is required\"}", 400);
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"cloud\": \"cloud1\"}", Request.Method.PATCH),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Field 'version' is required\"}", 400);
-
- // Error: Invalid versions
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"0.0.0\", \"cloud\": \"cloud1\"}", Request.Method.PATCH),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid version '0.0.0'\"}", 400);
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"foo\", \"cloud\": \"cloud1\"}", Request.Method.PATCH),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid version 'foo': Invalid version component in 'foo': For input string: \\\"foo\\\"\"}", 400);
-
- // Error: Invalid cloud
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.6\", \"cloud\": \"foo\"}", Request.Method.PATCH),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Cloud 'foo' does not exist in this system\"}", 400);
-
- // Error: Downgrade OS
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.4.1\", \"cloud\": \"cloud2\"}", Request.Method.PATCH),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Cannot downgrade cloud 'cloud2' to version 7.4.1: Missing 'force' parameter\"}", 400);
-
- // Error: Change a pinned version
- assertResponse(new Request("http://localhost:8080/os/v1/", "{\"version\": \"7.5.3\", \"cloud\": \"cloud1\"}", Request.Method.PATCH),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Cannot upgrade cloud cloud1' to version 7.5.3: Current target is pinned. Add 'force' parameter to override\"}", 400);
-
- // Request firmware checks in all zones.
- assertResponse(new Request("http://localhost:8080/os/v1/firmware/", "", Request.Method.POST),
- "{\"message\":\"Requested firmware checks in prod.us-east-3, prod.us-west-1, prod.eu-west-1.\"}", 200);
-
- // Cancel firmware checks in all prod zones.
- assertResponse(new Request("http://localhost:8080/os/v1/firmware/prod/", "", Request.Method.DELETE),
- "{\"message\":\"Cancelled firmware checks in prod.us-east-3, prod.us-west-1, prod.eu-west-1.\"}", 200);
-
- // Request firmware checks in prod.us-east-3.
- assertResponse(new Request("http://localhost:8080/os/v1/firmware/prod/us-east-3", "", Request.Method.POST),
- "{\"message\":\"Requested firmware checks in prod.us-east-3.\"}", 200);
-
- // Error: Cancel firmware checks in an empty set of zones.
- assertResponse(new Request("http://localhost:8080/os/v1/firmware/dev/", "", Request.Method.DELETE),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"No zones at path '/os/v1/firmware/dev/'\"}", 404);
-
- // Error: Missing or invalid versions to certify
- assertResponse(new Request("http://localhost:8080/os/v1/certify/cloud1/7.5.2", "", Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Missing Vespa version in request body\"}", 400);
- assertResponse(new Request("http://localhost:8080/os/v1/certify/cloud1/7.5.2", "foo", Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid version component in 'foo': For input string: \\\"foo\\\"\"}", 400);
- assertResponse(new Request("http://localhost:8080/os/v1/certify/cloud1/bar", "1.2.3", Request.Method.POST),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Invalid version component in 'bar': For input string: \\\"bar\\\"\"}", 400);
-
- assertFalse(tester.controller().auditLogger().readLog().entries().isEmpty(), "Actions are logged to audit log");
- }
-
- private void upgradeAndUpdateStatus() {
- osUpgraders.forEach(ControllerMaintainer::run);
- updateVersionStatus();
- }
-
- private void updateVersionStatus() {
- tester.controller().os().updateStatus(OsVersionStatus.compute(tester.controller()));
- }
-
- private void completeUpgrade(ZoneId... zones) {
- for (ZoneId zone : zones) {
- for (SystemApplication application : SystemApplication.all()) {
- var targetVersion = nodeRepository().targetVersionsOf(zone).osVersion(application.nodeType());
- for (Node node : nodeRepository().list(zone, NodeFilter.all().applications(application.id()))) {
- var version = targetVersion.orElse(node.wantedOsVersion());
- nodeRepository().putNodes(zone, Node.builder(node).currentOsVersion(version).wantedOsVersion(version).build());
- }
- }
- }
- updateVersionStatus();
- }
-
- private NodeRepositoryMock nodeRepository() {
- return tester.serviceRegistry().configServerMock().nodeRepository();
- }
-
- private void assertResponse(Request request, String body, int statusCode) {
- addIdentityToRequest(request, operator);
- tester.assertResponse(request, body, statusCode);
- }
-
- private void assertFile(Request request, String filename) {
- addIdentityToRequest(request, operator);
- tester.assertResponse(request, new File(filename));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json
deleted file mode 100644
index e3d8f877330..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-all-upgraded.json
+++ /dev/null
@@ -1,162 +0,0 @@
-{
- "versions": [
- {
- "version": "7.5.2",
- "targetVersion": true,
- "upgradeBudget": "PT0S",
- "scheduledAt": 1234,
- "pinned": false,
- "cloud": "cloud1",
- "nodes": [
- {
- "hostname": "node-2-configserver-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-1-configserver-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-3-configserver-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-1-configserver-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-2-configserver-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-3-configserver-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-2-proxy-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-3-proxy-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-1-proxy-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-2-proxy-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-1-proxy-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-3-proxy-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-3-tenant-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-2-tenant-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-1-tenant-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-3-tenant-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-2-tenant-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-1-tenant-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- }
- ]
- },
- {
- "version": "8.2.1",
- "targetVersion": true,
- "upgradeBudget": "PT0S",
- "scheduledAt": 1234,
- "pinned": false,
- "nextVersion": "8.2.1.20211228",
- "nextScheduledAt": 1640671200000,
- "certified": true,
- "cloud": "cloud2",
- "nodes": [
- {
- "hostname": "node-1-configserver-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-2-configserver-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-3-configserver-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-1-proxy-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-3-proxy-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-2-proxy-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-1-tenant-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-3-tenant-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-2-tenant-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json
deleted file mode 100644
index d3b5ac06982..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/os/responses/versions-partially-upgraded.json
+++ /dev/null
@@ -1,175 +0,0 @@
-{
- "versions": [
- {
- "version": "0.0.0",
- "targetVersion": false,
- "cloud": "cloud1",
- "nodes": [
- {
- "hostname": "node-1-configserver-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-2-configserver-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-3-configserver-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-2-proxy-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-1-proxy-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-3-proxy-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-3-tenant-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-2-tenant-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- },
- {
- "hostname": "node-1-tenant-host-prod.us-west-1",
- "environment": "prod",
- "region": "us-west-1"
- }
- ]
- },
- {
- "version": "7.5.2",
- "targetVersion": true,
- "upgradeBudget": "PT0S",
- "scheduledAt": 1234,
- "pinned": false,
- "cloud": "cloud1",
- "nodes": [
- {
- "hostname": "node-2-configserver-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-1-configserver-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-3-configserver-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-2-proxy-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-3-proxy-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-1-proxy-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-3-tenant-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-2-tenant-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- },
- {
- "hostname": "node-1-tenant-host-prod.us-east-3",
- "environment": "prod",
- "region": "us-east-3"
- }
- ]
- },
- {
- "version": "0.0.0",
- "targetVersion": false,
- "cloud": "cloud2",
- "nodes": [
- {
- "hostname": "node-1-configserver-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-2-configserver-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-3-configserver-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-1-proxy-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-3-proxy-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-2-proxy-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-1-tenant-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-3-tenant-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- },
- {
- "hostname": "node-2-tenant-host-prod.eu-west-1",
- "environment": "prod",
- "region": "eu-west-1"
- }
- ]
- },
- {
- "version": "8.2.1",
- "targetVersion": true,
- "upgradeBudget": "PT0S",
- "scheduledAt": 1234,
- "pinned": false,
- "nextVersion": "8.2.1.20211228",
- "nextScheduledAt": 1640671200000,
- "certified": true,
- "cloud": "cloud2",
- "nodes": [ ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/AllowingFilter.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/AllowingFilter.java
deleted file mode 100644
index 9798f95e703..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/AllowingFilter.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.playground;
-
-import com.yahoo.jdisc.http.filter.DiscFilterRequest;
-import com.yahoo.jdisc.http.filter.security.base.JsonSecurityRequestFilterBase;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzPrincipal;
-import com.yahoo.vespa.athenz.api.AthenzUser;
-import com.yahoo.jdisc.http.filter.security.misc.User;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.yolean.Exceptions;
-
-import java.time.Instant;
-import java.util.Optional;
-import java.util.Set;
-
-public class AllowingFilter extends JsonSecurityRequestFilterBase {
-
- static final AthenzPrincipal user = new AthenzPrincipal(new AthenzUser("demo"));
- static final AthenzDomain domain = new AthenzDomain("domain");
-
- @Override
- protected Optional<ErrorResponse> filter(DiscFilterRequest request) {
- try {
- request.setUserPrincipal(user);
- request.setAttribute(User.ATTRIBUTE_NAME, new User("mail@mail", user.getName(), "demo", null, true, -1, User.NO_DATE));
- request.setAttribute("okta.identity-token", "okta-it");
- request.setAttribute("okta.access-token", "okta-at");
- request.setAttribute(SecurityContext.ATTRIBUTE_NAME,
- new SecurityContext(user,
- Set.of(Role.hostedOperator()),
- Instant.now().minusSeconds(3600)));
- return Optional.empty();
- }
- catch (Throwable t) {
- return Optional.of(new ErrorResponse(500, Exceptions.toMessageString(t)));
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/DeploymentPlayground.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/DeploymentPlayground.java
deleted file mode 100644
index 38ae1502b6e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/DeploymentPlayground.java
+++ /dev/null
@@ -1,327 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.playground;
-
-import ai.vespa.validation.StringWrapper;
-import com.yahoo.application.Networking;
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.athenz.AthenzDbMock;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.time.Duration;
-import java.time.Instant;
-import java.util.ArrayDeque;
-import java.util.Comparator;
-import java.util.Deque;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.BiConsumer;
-import java.util.function.BooleanSupplier;
-import java.util.function.Predicate;
-import java.util.stream.Collectors;
-
-import static java.util.Comparator.comparing;
-import static java.util.Comparator.naturalOrder;
-import static java.util.function.Predicate.not;
-import static java.util.stream.Collectors.toSet;
-
-public class DeploymentPlayground extends ControllerContainerTest {
-
- private final Object monitor = new Object();
- private DeploymentTester deploymentTester;
-
- @Override
- protected Networking networking() { return Networking.enable; }
-
- public static void main(String[] args) throws IOException, InterruptedException {
- DeploymentPlayground test = null;
- try {
- test = new DeploymentPlayground();
- test.startContainer();
- test.run();
- }
- catch (Throwable t) {
- t.printStackTrace();
- }
- if (test != null && test.container != null) {
- test.stopContainer();
- test.deploymentTester.runner().shutdown();
- test.deploymentTester.upgrader().shutdown();
- test.deploymentTester.readyJobsTrigger().shutdown();
- test.deploymentTester.outstandingChangeDeployer().shutdown();
- }
- }
-
- public void run() throws IOException {
- ContainerTester tester = new ContainerTester(container, "");
- deploymentTester = new DeploymentTester(new ControllerTester(tester));
- deploymentTester.controllerTester().computeVersionStatus();
-
- AthenzDbMock.Domain domainMock = tester.athenzClientFactory().getSetup().getOrCreateDomain(AllowingFilter.domain);
- domainMock.markAsVespaTenant();
- domainMock.admin(AllowingFilter.user.getIdentity());
-
- ApplicationPackage applicationPackage = ApplicationPackageBuilder.fromDeploymentXml(readDeploymentXml());
- Map<String, DeploymentContext> instances = new LinkedHashMap<>();
- for (var instance : applicationPackage.deploymentSpec().instances())
- instances.put(instance.name().value(), deploymentTester.newDeploymentContext("demo", "app", instance.name().value()));
-
- instances.values().iterator().next().submit(applicationPackage);
-
- repl(instances);
- }
-
- static String readDeploymentXml() throws IOException {
- return Files.readString(Paths.get(System.getProperty("user.home") + "/git/" +
- "vespa/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment.xml"));
- }
-
- void repl(Map<String, DeploymentContext> instances) throws IOException {
- String[] command;
- BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
- AtomicBoolean on = new AtomicBoolean();
- Thread auto = new Thread(() -> auto(instances, on));
- auto.setDaemon(true);
- auto.start();
- while (true) {
- try {
- command = in.readLine().trim().split("\\s+");
- if (command.length == 0 || command[0].isEmpty()) continue;
- synchronized (monitor) {
- switch (command[0]) {
- case "exit":
- auto.interrupt();
- return;
- case "tick":
- deploymentTester.controllerTester().computeVersionStatus();
- deploymentTester.outstandingChangeDeployer().run();
- deploymentTester.upgrader().run();
- deploymentTester.triggerJobs();
- deploymentTester.runner().run();
- break;
- case "run":
- run(instances.get(command[1]), DeploymentContext::runJob, List.of(command).subList(2, command.length));
- break;
- case "fail":
- run(instances.get(command[1]), DeploymentContext::failDeployment, List.of(command).subList(2, command.length));
- break;
- case "upgrade":
- deploymentTester.controllerTester().upgradeSystem(new Version(command[1]));
- deploymentTester.controllerTester().computeVersionStatus();
- break;
- case "submit":
- instances.values().iterator().next().submit(ApplicationPackageBuilder.fromDeploymentXml(readDeploymentXml(),
- ValidationId.deploymentRemoval),
- command.length == 1 ? 2 : Integer.parseInt(command[1]));
- break;
- case "resubmit":
- instances.values().iterator().next().resubmit(ApplicationPackageBuilder.fromDeploymentXml(readDeploymentXml(),
- ValidationId.deploymentRemoval));
- break;
- case "advance":
- deploymentTester.clock().advance(Duration.ofMinutes(Long.parseLong(command[1])));
- break;
- case "auto":
- switch (command[1]) {
- case "on": on.set(true); break;
- case "off": on.set(false); break;
- default: System.err.println("Argument to 'auto' must be 'on' or 'off'"); break;
- }
- break;
- default:
- System.err.println("Cannot run '" + String.join(" ", command) + "'");
- }
- Set<String> names = instances.values().iterator().next().application().deploymentSpec().instanceNames().stream().map(StringWrapper::value).collect(toSet());
- instances.keySet().removeIf(not(names::contains));
- names.removeIf(instances.keySet()::contains);
- for (String name : names)
- instances.put(name, deploymentTester.newDeploymentContext("demo", "app", name));
- }
- }
- catch (Throwable t) {
- t.printStackTrace();
- }
- }
- }
-
- void auto(Map<String, DeploymentContext> instances, AtomicBoolean on) {
- while ( ! Thread.currentThread().isInterrupted()) {
- try {
- synchronized (monitor) {
- monitor.wait(1000);
- if ( ! on.get())
- continue;
- }
-
- deploymentTester.clock().advance(Duration.ofSeconds(60));
- deploymentTester.runner().run();
- deploymentTester.triggerJobs();
- deploymentTester.outstandingChangeDeployer().run();
- deploymentTester.controllerTester().computeVersionStatus();
- deploymentTester.upgrader().run();
-
- synchronized (monitor) {
- monitor.wait(1000);
- if ( ! on.get())
- continue;
- }
- deploymentTester.clock().advance(Duration.ofSeconds(60));
-
- List<Run> active = deploymentTester.jobs().active();
- if ( ! active.isEmpty()) {
- Run run = active.stream()
- .min(comparing(current -> deploymentTester.jobs().last(current.id().job()).map(Run::start)
- .orElse(Instant.EPOCH))).get();
- if (run.versions().sourcePlatform().map(run.versions().targetPlatform()::equals).orElse(true) || Math.random() < 0.4) {
- instances.get(run.id().application().instance().value()).runJob(run.id().type());
- }
- }
-
- }
- catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- catch (Throwable t) {
- t.printStackTrace();
- }
- }
- }
-
- /**
- void auto(Map<String, DeploymentContext> instances, AtomicBoolean on) {
- BooleanSupplier runJob = () -> {
- List<Run> active = deploymentTester.jobs().active();
- if ( ! active.isEmpty()) {
- Run run = active.stream()
- .min(comparing(current -> deploymentTester.jobs().last(current.id().job()).map(Run::start)
- .orElse(Instant.EPOCH))).get();
- if (run.versions().sourcePlatform().map(run.versions().targetPlatform()::equals).orElse(true) || Math.random() < 0.4) {
- instances.get(run.id().application().instance().value()).runJob(run.id().type());
- return false;
- }
- }
- return true;
- };
- List<Runnable> defaultTasks = List.of(() -> deploymentTester.runner().run(),
- () -> deploymentTester.triggerJobs(),
- () -> deploymentTester.outstandingChangeDeployer().run(),
- () -> deploymentTester.upgrader().run(),
- () -> deploymentTester.controllerTester().computeVersionStatus());
- while ( ! Thread.currentThread().isInterrupted()) {
- Deque<BooleanSupplier> tasks = new ArrayDeque<>();
- tasks.push(runJob);
- for (Runnable task : defaultTasks)
- tasks.push(() -> {
- task.run();
- return true;
- });
-
- while ( ! tasks.isEmpty())
- try {
- synchronized (monitor) {
- monitor.wait(1000);
- if ( ! on.get())
- break;
-
- deploymentTester.clock().advance(Duration.ofSeconds(60));
- BooleanSupplier task = tasks.pop();
- if ( ! task.getAsBoolean())
- tasks.push(task);
- }
- }
- catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- catch (Throwable t) {
- t.printStackTrace();
- }
- }
- }
- */
-
- void run(DeploymentContext instance, BiConsumer<DeploymentContext, JobType> action, List<String> jobs) {
- Set<JobId> haveRun = new HashSet<>();
- boolean triggered = true;
- while (triggered) {
- List<Run> runs = deploymentTester.jobs().active(instance.instanceId());
- triggered = false;
- for (Run run : runs)
- if (jobs.isEmpty() || jobs.contains(run.id().type().jobName().replace("production-", ""))) {
- if (haveRun.add(run.id().job())) {
- action.accept(instance, run.id().type());
- deploymentTester.triggerJobs();
- triggered = true;
- }
- }
- }
- }
-
- @Override
- protected String variablePartXml() {
- return """
- <component id='com.yahoo.vespa.hosted.controller.security.AthenzAccessControlRequests'/>
- <component id='com.yahoo.vespa.hosted.controller.athenz.impl.AthenzFacade'/>
-
- <handler id='com.yahoo.vespa.hosted.controller.restapi.application.ApplicationApiHandler'>
- <binding>http://localhost/application/v4/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.athenz.AthenzApiHandler'>
- <binding>http://localhost/athenz/v1/*</binding>
- </handler>
- <handler id='com.yahoo.vespa.hosted.controller.restapi.zone.v1.ZoneApiHandler'>
- <binding>http://localhost/zone/v1</binding>
- <binding>http://localhost/zone/v1/*</binding>
- </handler>
-
- <http>
- <server id='default' port='8080' />
- <filtering>
- <request-chain id='default'>
- <filter id='com.yahoo.jdisc.http.filter.security.cors.CorsPreflightRequestFilter'>
- <config name="jdisc.http.filter.security.cors.cors-filter">
- <allowedUrls>
- <item>http://localhost:3000</item>
- <item>http://localhost:8080</item>
- </allowedUrls>
- </config>
- </filter>
- <filter id='com.yahoo.vespa.hosted.controller.restapi.playground.AllowingFilter'/>
- <filter id='com.yahoo.vespa.hosted.controller.restapi.filter.ControllerAuthorizationFilter'/>
- <binding>http://localhost/*</binding>
- </request-chain>
- <response-chain id='responses'>
- <filter id='com.yahoo.jdisc.http.filter.security.cors.CorsResponseFilter'>
- <config name="jdisc.http.filter.security.cors.cors-filter"> <allowedUrls>
- <item>http://localhost:3000</item>
- <item>http://localhost:8080</item>
- </allowedUrls>
- </config>
- </filter>
- <binding>http://localhost/*</binding>
- </response-chain>
- </filtering>
- </http>
- """;
- }
-
-}
-
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment.xml b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment.xml
deleted file mode 100644
index 1097a197d96..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment.xml
+++ /dev/null
@@ -1,133 +0,0 @@
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<deployment version='1.0'>
- <notifications>
- <email role="author" />
- </notifications>
-
- <parallel>
- <instance id="omega"> <!-- Eats extra system tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <steps>
- <parallel>
- <instance id="alpha-1"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="alpha-2"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="alpha-3"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
- </parallel>
-
- <instance id="alpha-4"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- <region>us-west-1</region>
- </prod>
- </instance>
-
- <instance id="beta">
- <!-- Consider allowing risk based rollout with when-failing ... -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='3' max-risk='12' max-idle-hours='8' />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- <delay hours='1' />
- <test>us-west-1</test>
- </prod>
- </instance>
-
- <parallel>
- <instance id="prod5">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- <delay hours='8' />
- <test>us-west-1</test>
- </prod>
- </instance>
- </parallel>
-
- <parallel>
- <instance id="prod15">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-central-1</region>
- <region>eu-west-1</region>
- <region>aws-us-east-1a</region>
- </parallel>
- <delay hours='8' />
- <test>us-central-1</test>
- </prod>
- </instance>
-
- <instance id="prod25">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-west-1</region>
- <region>us-east-3</region>
- <region>ap-northeast-1</region>
- </parallel>
- <delay hours='8' />
- <test>us-west-1</test>
- </prod>
- </instance>
- </parallel>
-
- <parallel>
- <instance id="prod50">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-central-1</region>
- <region>eu-west-1</region>
- <region>aws-us-east-1a</region>
- </parallel>
- </prod>
- </instance>
-
- <instance id="prod100">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-west-1</region>
- <region>us-east-3</region>
- <region>ap-northeast-1</region>
- </parallel>
- </prod>
- </instance>
- </parallel>
-
- </steps>
- </parallel>
-
-</deployment>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_alt_full.xml b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_alt_full.xml
deleted file mode 100644
index 1097a197d96..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_alt_full.xml
+++ /dev/null
@@ -1,133 +0,0 @@
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<deployment version='1.0'>
- <notifications>
- <email role="author" />
- </notifications>
-
- <parallel>
- <instance id="omega"> <!-- Eats extra system tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <steps>
- <parallel>
- <instance id="alpha-1"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="alpha-2"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="alpha-3"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
- </parallel>
-
- <instance id="alpha-4"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- <region>us-west-1</region>
- </prod>
- </instance>
-
- <instance id="beta">
- <!-- Consider allowing risk based rollout with when-failing ... -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='3' max-risk='12' max-idle-hours='8' />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- <delay hours='1' />
- <test>us-west-1</test>
- </prod>
- </instance>
-
- <parallel>
- <instance id="prod5">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- <delay hours='8' />
- <test>us-west-1</test>
- </prod>
- </instance>
- </parallel>
-
- <parallel>
- <instance id="prod15">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-central-1</region>
- <region>eu-west-1</region>
- <region>aws-us-east-1a</region>
- </parallel>
- <delay hours='8' />
- <test>us-central-1</test>
- </prod>
- </instance>
-
- <instance id="prod25">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-west-1</region>
- <region>us-east-3</region>
- <region>ap-northeast-1</region>
- </parallel>
- <delay hours='8' />
- <test>us-west-1</test>
- </prod>
- </instance>
- </parallel>
-
- <parallel>
- <instance id="prod50">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-central-1</region>
- <region>eu-west-1</region>
- <region>aws-us-east-1a</region>
- </parallel>
- </prod>
- </instance>
-
- <instance id="prod100">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-west-1</region>
- <region>us-east-3</region>
- <region>ap-northeast-1</region>
- </parallel>
- </prod>
- </instance>
- </parallel>
-
- </steps>
- </parallel>
-
-</deployment>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_alternative.xml b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_alternative.xml
deleted file mode 100644
index b074792f716..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_alternative.xml
+++ /dev/null
@@ -1,68 +0,0 @@
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<deployment version='1.0'>
- <notifications>
- <email role="author" />
- </notifications>
-
- <parallel>
- <instance id="omega"> <!-- Eats extra system tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <steps>
- <parallel>
- <instance id="alpha-1"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="alpha-2"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="alpha-3"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
- </parallel>
-
- <instance id="alpha-4"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-east-3</region>
- <test>us-east-3</test>
- <region>us-west-1</region>
- </prod>
- </instance>
-
- <instance id="beta">
- <!-- Consider allowing risk based rollout with when-failing ... -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='3' max-risk='12' max-idle-hours='8' />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- <delay hours='1' />
- <test>us-west-1</test>
- </prod>
- </instance>
-
- <instance id="prod">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- </prod>
- </instance>
- </steps>
- </parallel>
-
-</deployment>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_base.xml b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_base.xml
deleted file mode 100644
index 715ff4fdb3f..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_base.xml
+++ /dev/null
@@ -1,64 +0,0 @@
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<deployment version='1.0'>
- <notifications>
- <email role="author" />
- </notifications>
-
- <parallel>
- <instance id="omega"> <!-- Eats extra system tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <steps>
- <parallel>
- <instance id="alpha-1"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="alpha-2"> <!-- Runs one third of the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="alpha-3"> <!-- Runs one third of the system tests, and the stress tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- </prod>
- </instance>
- </parallel>
-
- <instance id="beta">
- <!-- Consider allowing risk based rollout with when-failing ... -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='3' max-risk='12' max-idle-hours='8' />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- <delay hours='1' />
- <test>us-west-1</test>
- </prod>
- </instance>
-
- <instance id="prod">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- </prod>
- </instance>
- </steps>
- </parallel>
-
-</deployment>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_full.xml b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_full.xml
deleted file mode 100644
index 0b85852abdb..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_full.xml
+++ /dev/null
@@ -1,128 +0,0 @@
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<deployment version='1.0'>
- <notifications>
- <email role="author" />
- </notifications>
-
- <parallel>
- <instance id="omega"> <!-- Eats extra system and staging tests -->
- <upgrade policy='canary' revision-target='latest' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- </instance>
-
- <steps>
- <parallel>
- <instance id="alpha-1"> <!-- Runs half the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="alpha-2"> <!-- Runs half the system and staging tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="alpha-3"> <!-- Runs one third of the system tests, and the stress tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- </prod>
- </instance>
- </parallel>
-
- <instance id="beta">
- <!-- Consider allowing risk based rollout with when-failing ... -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='3' max-risk='12' max-idle-hours='8' />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- <delay hours='1' />
- <test>us-west-1</test>
- </prod>
- </instance>
-
- <parallel>
- <instance id="prod5">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- <delay hours='8' />
- <test>us-west-1</test>
- </prod>
- </instance>
- </parallel>
-
- <parallel>
- <instance id="prod15">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-central-1</region>
- <region>eu-west-1</region>
- <region>aws-us-east-1a</region>
- </parallel>
- <delay hours='8' />
- <test>us-central-1</test>
- </prod>
- </instance>
-
- <instance id="prod25">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-west-1</region>
- <region>us-east-3</region>
- <region>ap-northeast-1</region>
- </parallel>
- <delay hours='8' />
- <test>us-west-1</test>
- </prod>
- </instance>
- </parallel>
-
- <parallel>
- <instance id="prod50">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-central-1</region>
- <region>eu-west-1</region>
- <region>aws-us-east-1a</region>
- </parallel>
- </prod>
- </instance>
-
- <instance id="prod100">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <parallel>
- <region>us-west-1</region>
- <region>us-east-3</region>
- <region>ap-northeast-1</region>
- </parallel>
- </prod>
- </instance>
- </parallel>
-
- </steps>
- </parallel>
-
-</deployment>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simple.xml b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simple.xml
deleted file mode 100644
index 16986df174c..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simple.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<deployment version='1.0'>
-
- <parallel>
- <instance id="omega"> <!-- Eats extra system tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <steps>
- <instance id="alpha"> <!-- Runs system and stress tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region> <!-- Not really a production deployment, but used in stress tests -->
- </prod>
- </instance>
-
- <instance id="beta">
- <!-- Consider allowing risk based rollout with when-failing ... -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='3' max-risk='12' max-idle-hours='8' />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- <delay hours='1' />
- <test>us-west-1</test>
- </prod>
- </instance>
-
- <instance id="prod">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- </prod>
- </instance>
- </steps>
- </parallel>
-
-</deployment>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simpler.xml b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simpler.xml
deleted file mode 100644
index 8be40e84495..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simpler.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<deployment version='1.0'>
-
- <instance id="alpha"> <!-- Runs system and stress tests -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-failing' rollout='separate' />
- <test />
- <staging />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- </instance>
-
- <instance id="beta">
- <!-- Consider allowing risk based rollout with when-failing ... -->
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='3' max-risk='12' max-idle-hours='8' />
- <!--block-change revision="false" days="sun,mon,tue,thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- <delay hours='1' />
- <test>us-west-1</test>
- </prod>
- </instance>
-
- <instance id="prod">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' min-risk='6' max-risk='12' max-idle-hours='32' />
- <!--block-change version="false" days="thu,fri,sat" hours="0-23" time-zone="UTC"/-->
- <!--block-change revision="false" days="sun,mon,tue,wed,fri,sat" hours="0-23" time-zone="UTC"/-->
- <prod>
- <region>us-west-1</region>
- </prod>
- </instance>
-
-</deployment>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simplest.xml b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simplest.xml
deleted file mode 100644
index 37efaf82b5a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/playground/deployment_simplest.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
-<deployment version='1.0'>
-
- <instance id="beta"> <!-- Runs system and production tests -->
- <test />
- <staging />
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' />
- <prod>
- <region>us-west-1</region>
- <delay hours='1' />
- <test>us-west-1</test>
- </prod>
- </instance>
-
- <instance id="prod5">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' />
- <prod>
- <region>us-west-1</region>
- <delay hours='8' />
- <test>us-west-1</test>
- </prod>
- </instance>
-
- <instance id="prod25">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' />
- <prod>
- <region>us-west-1</region>
- <delay hours='8' />
- <test>us-west-1</test>
- </prod>
- </instance>
-
- <instance id="prod100">
- <upgrade policy='conservative' revision-target='next' revision-change='when-clear' rollout='separate' />
- <prod>
- <region>us-west-1</region>
- </prod>
- </instance>
-
-</deployment>
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java
deleted file mode 100644
index 309501f5679..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/RoutingApiTest.java
+++ /dev/null
@@ -1,343 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.routing;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.component.Version;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.AthenzService;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
-
-/**
- * @author mpolden
- */
-public class RoutingApiTest extends ControllerContainerTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/";
-
- private ContainerTester tester;
- private DeploymentTester deploymentTester;
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, responseFiles);
- deploymentTester = new DeploymentTester(new ControllerTester(tester));
- deploymentTester.controllerTester().upgradeSystem(Version.fromString("6.1"));
- }
-
- @Test
- void discovery() {
- // Deploy
- var context = deploymentTester.newDeploymentContext("t1", "a1", "default");
- var westZone = ZoneId.from("prod", "us-west-1");
- var eastZone = ZoneId.from("prod", "us-east-3");
- var applicationPackage = new ApplicationPackageBuilder()
- .region(westZone.region())
- .region(eastZone.region())
- .endpoint("default", "default", eastZone.region().value(), westZone.region().value())
- .build();
- context.submit(applicationPackage).deploy();
-
- // GET root
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/", "",
- Request.Method.GET),
- new File("discovery/root.json"));
-
- // GET tenant
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/t1", "",
- Request.Method.GET),
- new File("discovery/tenant.json"));
-
- // GET application
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/t1/application/a1/",
- "",
- Request.Method.GET),
- new File("discovery/application.json"));
-
- // GET instance
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/t1/application/a1/instance/default/",
- "",
- Request.Method.GET),
- new File("discovery/instance.json"));
-
- // GET environment
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/", "",
- Request.Method.GET),
- new File("discovery/environment.json"));
- }
-
- @Test
- void recursion() {
- var context1 = deploymentTester.newDeploymentContext("t1", "a1", "default");
- var westZone = ZoneId.from("prod", "us-west-1");
- var eastZone = ZoneId.from("prod", "us-east-3");
- var package1 = new ApplicationPackageBuilder()
- .region(westZone.region())
- .region(eastZone.region())
- .endpoint("default", "default", eastZone.region().value(), westZone.region().value())
- .build();
- context1.submit(package1).deploy();
-
- var context2 = deploymentTester.newDeploymentContext("t1", "a2", "default");
- var package2 = new ApplicationPackageBuilder()
- .region(westZone.region())
- .region(eastZone.region())
- .endpoint("default", "default", eastZone.region().value(), westZone.region().value())
- .build();
- context2.submit(package2).deploy();
-
- // GET tenant recursively
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/t1?recursive=true", "",
- Request.Method.GET),
- new File("recursion/tenant.json"));
-
- // GET application recursively
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/t1/application/a1?recursive=true", "",
- Request.Method.GET),
- new File("recursion/application.json"));
-
- // GET instance recursively
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/t1/application/a1/instance/default?recursive=true", "",
- Request.Method.GET),
- new File("recursion/application.json"));
-
- // GET environment recursively
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment?recursive=true", "",
- Request.Method.GET),
- new File("recursion/environment.json"));
- }
-
- @Test
- void exclusive_routing() {
- var context = deploymentTester.newDeploymentContext();
- // Zones support direct routing
- var westZone = ZoneId.from("prod", "us-west-1");
- var eastZone = ZoneId.from("prod", "us-east-3");
- deploymentTester.controllerTester().zoneRegistry().exclusiveRoutingIn(ZoneApiMock.from(westZone),
- ZoneApiMock.from(eastZone));
- // Deploy application
- var applicationPackage = new ApplicationPackageBuilder()
- .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"))
- .region(westZone.region())
- .region(eastZone.region())
- .endpoint("default", "default", eastZone.region().value(), westZone.region().value())
- .build();
- context.submit(applicationPackage).deploy();
-
- // GET initial deployment status
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("policy/deployment-status-initial.json"));
-
- // POST sets deployment out
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.POST),
- "{\"message\":\"Set global routing status for tenant.application in prod.us-west-1 to OUT\"}");
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("policy/deployment-status-out.json"));
-
- // DELETE sets deployment in
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.DELETE),
- "{\"message\":\"Set global routing status for tenant.application in prod.us-west-1 to IN\"}");
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("policy/deployment-status-in.json"));
-
- // GET initial zone status
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("policy/zone-status-initial.json"));
-
- // POST sets zone out
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/environment/prod/region/us-west-1",
- "", Request.Method.POST),
- "{\"message\":\"Set global routing status for deployments in prod.us-west-1 to OUT\"}");
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("policy/zone-status-out.json"));
-
- // DELETE sets zone in
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/environment/prod/region/us-west-1",
- "", Request.Method.DELETE),
- "{\"message\":\"Set global routing status for deployments in prod.us-west-1 to IN\"}");
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("policy/zone-status-in.json"));
-
- // Endpoint is removed
- applicationPackage = new ApplicationPackageBuilder()
- .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"))
- .region(westZone.region())
- .region(eastZone.region())
- .allow(ValidationId.globalEndpointChange)
- .build();
- context.submit(applicationPackage).deploy();
-
- // GET deployment status. Now empty as no routing policies have global endpoints
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- "{\"deployments\":[]}");
- }
-
- @Test
- void shared_routing() {
- // Deploy application
- var context = deploymentTester.newDeploymentContext();
- var westZone = ZoneId.from("prod", "us-west-1");
- var eastZone = ZoneId.from("prod", "us-east-3");
- var applicationPackage = new ApplicationPackageBuilder()
- .region(westZone.region())
- .region(eastZone.region())
- .endpoint("default", "default", eastZone.region().value(), westZone.region().value())
- .build();
- context.submit(applicationPackage).deploy();
-
- assertNotEquals(List.of(), context.instance().rotations(), "Rotation is assigned");
-
- // GET initial deployment status
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("rotation/deployment-status-initial.json"));
-
- // GET initial deployment status: unknown instance
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/foo/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Unknown instance 'foo' in 'tenant.application'\"}",
- 400);
-
- // POST sets deployment out
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.POST),
- "{\"message\":\"Set global routing status for tenant.application in prod.us-west-1 to OUT\"}");
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("rotation/deployment-status-out.json"));
-
- // DELETE sets deployment in
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.DELETE),
- "{\"message\":\"Set global routing status for tenant.application in prod.us-west-1 to IN\"}");
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("rotation/deployment-status-in.json"));
-
- // GET initial zone status
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("rotation/zone-status-initial.json"));
-
- // POST sets zone out
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/environment/prod/region/us-west-1",
- "", Request.Method.POST),
- "{\"message\":\"Set global routing status for deployments in prod.us-west-1 to OUT\"}");
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("rotation/zone-status-out.json"));
-
- // DELETE sets zone in
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/environment/prod/region/us-west-1",
- "", Request.Method.DELETE),
- "{\"message\":\"Set global routing status for deployments in prod.us-west-1 to IN\"}");
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("rotation/zone-status-in.json"));
- }
-
- @Test
- void mixed_routing_multiple_zones() {
- var westZone = ZoneId.from("prod", "us-west-1");
- var eastZone = ZoneId.from("prod", "us-east-3");
-
- // One shared and one exclusive zone
- deploymentTester.controllerTester().zoneRegistry().setRoutingMethod(ZoneApiMock.from(westZone),
- RoutingMethod.sharedLayer4);
- deploymentTester.controllerTester().zoneRegistry().setRoutingMethod(ZoneApiMock.from(eastZone),
- RoutingMethod.exclusive);
-
- // Deploy application
- var context = deploymentTester.newDeploymentContext();
- var applicationPackage = new ApplicationPackageBuilder()
- .region(westZone.region())
- .region(eastZone.region())
- .athenzIdentity(AthenzDomain.from("domain"), AthenzService.from("service"))
- .endpoint("endpoint1", "default", westZone.region().value())
- .endpoint("endpoint2", "default", eastZone.region().value())
- .build();
- context.submit(applicationPackage).deploy();
-
- // GET status for zone using sharedLayer4 routing
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- new File("rotation/deployment-status-initial.json"));
-
- // GET status for zone using exclusive routing
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-east-3",
- "", Request.Method.GET),
- "{\"deployments\":[{\"routingMethod\":\"exclusive\",\"instance\":\"tenant:application:default\"," +
- "\"environment\":\"prod\",\"region\":\"us-east-3\",\"status\":\"in\",\"agent\":\"system\",\"changedAt\":0}]}");
- }
-
- @Test
- void invalid_requests() {
- // GET non-existent application
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/t1/application/a1/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"t1.a1 not found\"}",
- 400);
-
- // GET, DELETE non-existent deployment
- var context = deploymentTester.newDeploymentContext();
- var applicationPackage = new ApplicationPackageBuilder()
- .region("us-east-3")
- .endpoint("default", "default")
- .build();
- context.submit(applicationPackage).deploy();
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.GET),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No such deployment: tenant.application in prod.us-west-1\"}",
- 400);
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/inactive/tenant/tenant/application/application/instance/default/environment/prod/region/us-west-1",
- "", Request.Method.DELETE),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No such deployment: tenant.application in prod.us-west-1\"}",
- 400);
-
- // GET non-existent zone
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/environment/prod/region/us-north-1",
- "", Request.Method.GET),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"No such zone: prod.us-north-1\"}",
- 400);
- }
-
- @Test
- void endpoints_list() {
- var context = deploymentTester.newDeploymentContext("t1", "a1", "default");
- var westZone = ZoneId.from("prod", "us-west-1");
- var eastZone = ZoneId.from("prod", "us-east-3");
- var applicationPackage = new ApplicationPackageBuilder()
- .region(westZone.region())
- .region(eastZone.region())
- .endpoint("default", "default", eastZone.region().value(), westZone.region().value())
- .build();
- context.submit(applicationPackage).deploy();
-
- tester.assertResponse(operatorRequest("http://localhost:8080/routing/v1/status/tenant/t1/application/a1/instance/default/endpoint", "", Request.Method.GET),
- new File("endpoint/endpoints.json"));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/application.json
deleted file mode 100644
index deda734cbbf..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/application.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "resources": [
- {
- "url": "http://localhost:8080/routing/v1/status/tenant/t1/application/a1/instance/default/"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/environment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/environment.json
deleted file mode 100644
index d8a64afb34e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/environment.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "resources": [
- {
- "url": "http://localhost:8080/routing/v1/status/environment/dev/region/aws-us-east-2a/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/dev/region/us-east-1/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/perf/region/us-east-3/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/prod/region/ap-northeast-1/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/prod/region/ap-northeast-2/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/prod/region/ap-southeast-1/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/prod/region/aws-us-east-1a/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/prod/region/aws-us-east-1b/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/prod/region/eu-west-1/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/prod/region/us-central-1/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/prod/region/us-east-3/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/prod/region/us-west-1/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/staging/region/us-east-3/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/test/region/us-east-1/"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/instance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/instance.json
deleted file mode 100644
index 1a3ad823e14..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/instance.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "resources": [
- {
- "url": "http://localhost:8080/routing/v1/status/tenant/t1/application/a1/instance/default/environment/prod/region/us-east-3/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/tenant/t1/application/a1/instance/default/environment/prod/region/us-west-1/"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/root.json
deleted file mode 100644
index 9b5630335aa..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/root.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "resources": [
- {
- "url": "http://localhost:8080/routing/v1/status/tenant/"
- },
- {
- "url": "http://localhost:8080/routing/v1/status/environment/"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/tenant.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/tenant.json
deleted file mode 100644
index acd05d35c8d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/discovery/tenant.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "resources": [
- {
- "url": "http://localhost:8080/routing/v1/status/tenant/t1/application/a1/"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/endpoint/endpoints.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/endpoint/endpoints.json
deleted file mode 100644
index 75369a19ea7..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/endpoint/endpoints.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "endpoints": [
- {
- "name": "default",
- "dnsName": "a1.t1.global.vespa.oath.cloud",
- "routingMethod": "sharedLayer4",
- "cluster": "default",
- "scope": "global",
- "zones": [
- {
- "routingMethod": "sharedLayer4",
- "instance": "t1:a1:default",
- "environment": "prod",
- "region": "us-east-3",
- "status": "in",
- "agent": "unknown",
- "changedAt": 1497618757000
- },
- {
- "routingMethod": "sharedLayer4",
- "instance": "t1:a1:default",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "unknown",
- "changedAt": 1497618757000
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-in.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-in.json
deleted file mode 100644
index 5301e8398eb..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-in.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "deployments": [
- {
- "routingMethod": "exclusive",
- "instance": "tenant:application:default",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 1600000000000
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-initial.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-initial.json
deleted file mode 100644
index 5383eb7f806..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-initial.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "deployments": [
- {
- "routingMethod": "exclusive",
- "instance": "tenant:application:default",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "system",
- "changedAt": 0
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-out.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-out.json
deleted file mode 100644
index 889aa199279..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/deployment-status-out.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "deployments": [
- {
- "routingMethod": "exclusive",
- "instance": "tenant:application:default",
- "environment": "prod",
- "region": "us-west-1",
- "status": "out",
- "agent": "operator",
- "changedAt": 1600000000000
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-in.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-in.json
deleted file mode 100644
index 0aa6d79ee63..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-in.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "routingMethod": "exclusive",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 1600000000000
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-initial.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-initial.json
deleted file mode 100644
index cb3fb75f5cb..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-initial.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "routingMethod": "exclusive",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "system",
- "changedAt": 0
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-out.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-out.json
deleted file mode 100644
index 1602ca76a9d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/policy/zone-status-out.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "routingMethod": "exclusive",
- "environment": "prod",
- "region": "us-west-1",
- "status": "out",
- "agent": "operator",
- "changedAt": 1600000000000
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/application.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/application.json
deleted file mode 100644
index 77c5d544f6d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/application.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "deployments": [
- {
- "routingMethod": "sharedLayer4",
- "instance": "t1:a1:default",
- "environment": "prod",
- "region": "us-east-3",
- "status": "in",
- "agent": "unknown",
- "changedAt": 1497618757000
- },
- {
- "routingMethod": "sharedLayer4",
- "instance": "t1:a1:default",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "unknown",
- "changedAt": 1497618757000
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/environment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/environment.json
deleted file mode 100644
index 3b085162393..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/environment.json
+++ /dev/null
@@ -1,116 +0,0 @@
-{
- "zones": [
- {
- "routingMethod": "sharedLayer4",
- "environment": "test",
- "region": "us-east-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- },
- {
- "routingMethod": "sharedLayer4",
- "environment": "staging",
- "region": "us-east-3",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- },
- {
- "routingMethod": "sharedLayer4",
- "environment": "dev",
- "region": "us-east-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- },
- {
- "routingMethod": "exclusive",
- "environment": "dev",
- "region": "aws-us-east-2a",
- "status": "in",
- "agent": "system",
- "changedAt": 0
- },
- {
- "routingMethod": "sharedLayer4",
- "environment": "perf",
- "region": "us-east-3",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- },
- {
- "routingMethod": "exclusive",
- "environment": "prod",
- "region": "aws-us-east-1a",
- "status": "in",
- "agent": "system",
- "changedAt": 0
- },
- {
- "routingMethod": "exclusive",
- "environment": "prod",
- "region": "aws-us-east-1b",
- "status": "in",
- "agent": "system",
- "changedAt": 0
- },
- {
- "routingMethod": "sharedLayer4",
- "environment": "prod",
- "region": "ap-northeast-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- },
- {
- "routingMethod": "sharedLayer4",
- "environment": "prod",
- "region": "ap-northeast-2",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- },
- {
- "routingMethod": "sharedLayer4",
- "environment": "prod",
- "region": "ap-southeast-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- },
- {
- "routingMethod": "sharedLayer4",
- "environment": "prod",
- "region": "us-east-3",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- },
- {
- "routingMethod": "sharedLayer4",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- },
- {
- "routingMethod": "sharedLayer4",
- "environment": "prod",
- "region": "us-central-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- },
- {
- "routingMethod": "sharedLayer4",
- "environment": "prod",
- "region": "eu-west-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/tenant.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/tenant.json
deleted file mode 100644
index 6ff308679f6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/recursion/tenant.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "deployments": [
- {
- "routingMethod": "sharedLayer4",
- "instance": "t1:a1:default",
- "environment": "prod",
- "region": "us-east-3",
- "status": "in",
- "agent": "unknown",
- "changedAt": 1497618757000
- },
- {
- "routingMethod": "sharedLayer4",
- "instance": "t1:a1:default",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "unknown",
- "changedAt": 1497618757000
- },
- {
- "routingMethod": "sharedLayer4",
- "instance": "t1:a2:default",
- "environment": "prod",
- "region": "us-east-3",
- "status": "in",
- "agent": "unknown",
- "changedAt": 1497618757000
- },
- {
- "routingMethod": "sharedLayer4",
- "instance": "t1:a2:default",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "unknown",
- "changedAt": 1497618757000
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-in.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-in.json
deleted file mode 100644
index e7c4f5842f5..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-in.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "deployments": [
- {
- "routingMethod": "sharedLayer4",
- "instance": "tenant:application:default",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 1600000000000
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-initial.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-initial.json
deleted file mode 100644
index 4a774c7b850..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-initial.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "deployments": [
- {
- "routingMethod": "sharedLayer4",
- "instance": "tenant:application:default",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "unknown",
- "changedAt": 1497618757000
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-out.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-out.json
deleted file mode 100644
index 16ac4eb907d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/deployment-status-out.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "deployments": [
- {
- "routingMethod": "sharedLayer4",
- "instance": "tenant:application:default",
- "environment": "prod",
- "region": "us-west-1",
- "status": "out",
- "agent": "operator",
- "changedAt": 1600000000000
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-in.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-in.json
deleted file mode 100644
index a6b873b5be5..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-in.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "routingMethod": "sharedLayer4",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-initial.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-initial.json
deleted file mode 100644
index a6b873b5be5..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-initial.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "routingMethod": "sharedLayer4",
- "environment": "prod",
- "region": "us-west-1",
- "status": "in",
- "agent": "operator",
- "changedAt": 0
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-out.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-out.json
deleted file mode 100644
index 3fd71a731eb..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/routing/responses/rotation/zone-status-out.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "routingMethod": "sharedLayer4",
- "environment": "prod",
- "region": "us-west-1",
- "status": "out",
- "agent": "operator",
- "changedAt": 0
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResultTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResultTest.java
deleted file mode 100644
index 44fc86e314e..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployResultTest.java
+++ /dev/null
@@ -1,76 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.systemflags;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.flags.FlagId;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.wire.WireSystemFlagsDeployResult;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock;
-import org.junit.jupiter.api.Test;
-
-import java.util.List;
-import java.util.Set;
-
-import static com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.FlagDataChange;
-import static com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.OperationError;
-import static com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.merge;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author bjorncs
- */
-public class SystemFlagsDeployResultTest {
- private final ZoneApiMock prodUsWest1Zone = ZoneApiMock.fromId("prod.us-west-1");
- private final ZoneRegistryMock registry = new ZoneRegistryMock(SystemName.cd).setZones(prodUsWest1Zone);
-
- @Test
- void changes_and_errors_are_present_in_wire_format() {
- FlagsTarget controllerTarget = FlagsTarget.forController(registry.systemZone());
- FlagId flagOne = new FlagId("flagone");
- FlagId flagTwo = new FlagId("flagtwo");
- SystemFlagsDeployResult result = new SystemFlagsDeployResult(
- List.of(FlagDataChange.deleted(flagOne, controllerTarget)),
- List.of(OperationError.deleteFailed("delete failed", controllerTarget, flagTwo)),
- List.of());
- WireSystemFlagsDeployResult wire = result.toWire();
-
- assertEquals(1, wire.changes.size());
- assertEquals(wire.changes.get(0).flagId, flagOne.toString());
- assertEquals(1, wire.errors.size());
- assertEquals(wire.errors.get(0).flagId, flagTwo.toString());
- }
-
- @Test
- void identical_errors_and_changes_from_multiple_targets_are_merged() {
- FlagsTarget prodUsWest1Target = FlagsTarget.forConfigServer(registry, prodUsWest1Zone);
- FlagsTarget controllerTarget = FlagsTarget.forController(registry.systemZone());
-
- FlagId flagOne = new FlagId("flagone");
- FlagId flagTwo = new FlagId("flagtwo");
-
- SystemFlagsDeployResult resultController =
- new SystemFlagsDeployResult(
- List.of(FlagDataChange.deleted(flagOne, controllerTarget)),
- List.of(OperationError.deleteFailed("message", controllerTarget, flagTwo)),
- List.of());
- SystemFlagsDeployResult resultProdUsWest1 =
- new SystemFlagsDeployResult(
- List.of(FlagDataChange.deleted(flagOne, prodUsWest1Target)),
- List.of(OperationError.deleteFailed("message", prodUsWest1Target, flagTwo)),
- List.of());
-
- var results = List.of(resultController, resultProdUsWest1);
- SystemFlagsDeployResult mergedResult = merge(results);
-
- List<FlagDataChange> changes = mergedResult.flagChanges();
- assertEquals(1, changes.size());
- FlagDataChange change = changes.get(0);
- assertEquals(change.targets(), Set.of(controllerTarget, prodUsWest1Target));
-
- List<OperationError> errors = mergedResult.errors();
- assertEquals(1, errors.size());
- OperationError error = errors.get(0);
- assertEquals(error.targets(), Set.of(controllerTarget, prodUsWest1Target));
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployerTest.java
deleted file mode 100644
index b53a0847ddb..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/systemflags/SystemFlagsDeployerTest.java
+++ /dev/null
@@ -1,258 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.systemflags;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagId;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.FlagsTarget;
-import com.yahoo.vespa.hosted.controller.api.systemflags.v1.SystemFlagsDataArchive;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneRegistryMock;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-
-import static com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.FlagDataChange;
-import static com.yahoo.vespa.hosted.controller.restapi.systemflags.SystemFlagsDeployResult.OperationError;
-import static com.yahoo.yolean.Exceptions.uncheck;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-/**
- * @author bjorncs
- */
-public class SystemFlagsDeployerTest {
-
- private static final SystemName SYSTEM = SystemName.main;
- private static final FlagId FLAG_ID = new FlagId("my-flag");
-
- private final ZoneApiMock prodUsWest1Zone = ZoneApiMock.fromId("prod.us-west-1");
- private final ZoneApiMock prodUsEast3Zone = ZoneApiMock.fromId("prod.us-east-3");
- private final ZoneRegistryMock registry = new ZoneRegistryMock(SYSTEM).setZones(prodUsWest1Zone, prodUsEast3Zone);
-
- private final FlagsTarget controllerTarget = FlagsTarget.forController(registry.systemZone());
- private final FlagsTarget prodUsWest1Target = FlagsTarget.forConfigServer(registry, prodUsWest1Zone);
- private final FlagsTarget prodUsEast3Target = FlagsTarget.forConfigServer(registry, prodUsEast3Zone);
-
- @Test
- void deploys_flag_data_to_targets() {
- FlagsClient flagsClient = mock(FlagsClient.class);
- when(flagsClient.listFlagData(controllerTarget)).thenReturn(List.of());
- when(flagsClient.listFlagData(prodUsWest1Target)).thenReturn(List.of(flagData("existing-prod.us-west-1.json")));
- FlagData existingProdUsEast3Data = flagData("existing-prod.us-east-3.json");
- when(flagsClient.listFlagData(prodUsEast3Target)).thenReturn(List.of(existingProdUsEast3Data));
-
- FlagData defaultData = flagData("flags/my-flag/main.json");
- FlagData prodUsEast3Data = flagData("flags/my-flag/main.prod.us-east-3.json");
- SystemFlagsDataArchive archive = new SystemFlagsDataArchive.Builder()
- .addFile("main.json", defaultData)
- .addFile("main.prod.us-east-3.json", prodUsEast3Data)
- .build();
-
- SystemFlagsDeployer deployer =
- new SystemFlagsDeployer(flagsClient, SYSTEM, Set.of(controllerTarget, prodUsWest1Target, prodUsEast3Target));
-
-
- SystemFlagsDeployResult result = deployer.deployFlags(archive, false);
-
- verify(flagsClient).putFlagData(controllerTarget, defaultData);
- verify(flagsClient).putFlagData(prodUsEast3Target, prodUsEast3Data);
- verify(flagsClient, never()).putFlagData(prodUsWest1Target, defaultData);
- List<FlagDataChange> changes = result.flagChanges();
-
- assertThat(changes).containsOnly(
- FlagDataChange.created(FLAG_ID, controllerTarget, defaultData),
- FlagDataChange.updated(FLAG_ID, prodUsEast3Target, prodUsEast3Data, existingProdUsEast3Data));
- }
-
- @Test
- void deploys_partial_flag_data_to_targets() {
- // default.json contains one rule with 2 conditions, one of which has a condition on the aws cloud.
- // This condition IS resolved for a config server target, but NOT for a controller target, because FLAG_ID
- // has the CLOUD dimension set.
- deployFlags(Optional.empty(), "partial/default.json", Optional.of("partial/put-controller.json"), true, PutType.CREATE, FetchVector.Dimension.CLOUD);
- deployFlags(Optional.empty(), "partial/default.json", Optional.empty(), false, PutType.NONE, FetchVector.Dimension.CLOUD);
- deployFlags(Optional.of("partial/initial.json"), "partial/default.json", Optional.of("partial/put-controller.json"), true, PutType.UPDATE, FetchVector.Dimension.CLOUD);
- deployFlags(Optional.of("partial/initial.json"), "partial/default.json", Optional.empty(), false, PutType.DELETE, FetchVector.Dimension.CLOUD);
-
- // When the CLOUD dimension is NOT set on the dimension, the controller target will also resolve that dimension, and
- // the result should be identical to the config server target. Let's also verify the config server target is unchanged.
- deployFlags(Optional.empty(), "partial/default.json", Optional.empty(), true, PutType.NONE);
- deployFlags(Optional.empty(), "partial/default.json", Optional.empty(), false, PutType.NONE);
- deployFlags(Optional.of("partial/initial.json"), "partial/default.json", Optional.empty(), true, PutType.DELETE);
- deployFlags(Optional.of("partial/initial.json"), "partial/default.json", Optional.empty(), false, PutType.DELETE);
- }
-
- private enum PutType {
- CREATE,
- UPDATE,
- DELETE,
- NONE
- }
-
- /**
- * @param existingFlagDataPath path to flag data the target already has
- * @param defaultFlagDataPath path to default json file
- * @param putFlagDataPath path to flag data pushed to target, or empty if nothing should be pushed
- * @param controller whether to target the controller, or config server
- */
- private void deployFlags(Optional<String> existingFlagDataPath,
- String defaultFlagDataPath,
- Optional<String> putFlagDataPath,
- boolean controller,
- PutType putType,
- FetchVector.Dimension... flagDimensions) {
- List<FlagData> existingFlagData = existingFlagDataPath.map(SystemFlagsDeployerTest::flagData).map(List::of).orElse(List.of());
- FlagData defaultFlagData = flagData(defaultFlagDataPath);
- FlagsTarget target = controller ? controllerTarget : prodUsWest1Target;
- Optional<FlagData> putFlagData = putFlagDataPath.map(SystemFlagsDeployerTest::flagData);
-
- try (var replacer = Flags.clearFlagsForTesting()) {
- Flags.defineStringFlag(FLAG_ID.toString(), "default", List.of("hakonhall"), "2023-07-27", "2123-07-27", "", "", flagDimensions);
-
- FlagsClient flagsClient = mock(FlagsClient.class);
- when(flagsClient.listFlagData(target)).thenReturn(existingFlagData);
-
- SystemFlagsDataArchive archive = new SystemFlagsDataArchive.Builder()
- .addFile("default.json", defaultFlagData)
- .build();
-
- SystemFlagsDeployer deployer = new SystemFlagsDeployer(flagsClient, SYSTEM, Set.of(target));
-
- List<FlagDataChange> changes = deployer.deployFlags(archive, false).flagChanges();
-
- putFlagData.ifPresentOrElse(flagData -> {
- verify(flagsClient).putFlagData(target, flagData);
- switch (putType) {
- case CREATE -> assertThat(changes).containsOnly(FlagDataChange.created(FLAG_ID, target, flagData));
- case UPDATE -> assertThat(changes).containsOnly(FlagDataChange.updated(FLAG_ID, target, flagData, existingFlagData.get(0)));
- case DELETE, NONE -> throw new IllegalStateException("Flag data put to the target, but change type is " + putType);
- }
- }, () -> {
- verify(flagsClient, never()).putFlagData(eq(target), any());
- switch (putType) {
- case DELETE -> assertThat(changes).containsOnly(FlagDataChange.deleted(FLAG_ID, target));
- case NONE -> assertEquals(changes, List.of());
- default -> throw new IllegalStateException("No flag data is expected to be put to the target but change type is " + putType);
- }
- });
- }
- }
-
- @Test
- void dryrun_should_not_change_flags() {
- FlagsClient flagsClient = mock(FlagsClient.class);
- when(flagsClient.listFlagData(controllerTarget)).thenReturn(List.of());
- when(flagsClient.listDefinedFlags(controllerTarget)).thenReturn(List.of(new FlagId("my-flag")));
-
- FlagData defaultData = flagData("flags/my-flag/main.json");
- SystemFlagsDataArchive archive = new SystemFlagsDataArchive.Builder()
- .addFile("main.json", defaultData)
- .build();
-
- SystemFlagsDeployer deployer = new SystemFlagsDeployer(flagsClient, SYSTEM, Set.of(controllerTarget));
- SystemFlagsDeployResult result = deployer.deployFlags(archive, true);
-
- verify(flagsClient, times(1)).listFlagData(controllerTarget);
- verify(flagsClient, never()).putFlagData(controllerTarget, defaultData);
- verify(flagsClient, never()).deleteFlagData(controllerTarget, FLAG_ID);
-
- assertThat(result.flagChanges()).containsOnly(
- FlagDataChange.created(FLAG_ID, controllerTarget, defaultData));
- assertTrue(result.errors().isEmpty());
- }
-
- @Test
- void creates_error_entries_in_result_if_flag_data_operations_fail() {
- FlagsClient flagsClient = mock(FlagsClient.class);
- UncheckedIOException exception = new UncheckedIOException(new IOException("I/O error message"));
- when(flagsClient.listFlagData(prodUsWest1Target)).thenThrow(exception);
- when(flagsClient.listFlagData(prodUsEast3Target)).thenReturn(List.of());
- when(flagsClient.listDefinedFlags(prodUsEast3Target)).thenReturn(List.of(new FlagId("my-flag")));
-
- FlagData defaultData = flagData("flags/my-flag/main.json");
- SystemFlagsDataArchive archive = new SystemFlagsDataArchive.Builder()
- .addFile("main.json", defaultData)
- .build();
-
- SystemFlagsDeployer deployer = new SystemFlagsDeployer(flagsClient, SYSTEM, Set.of(prodUsWest1Target, prodUsEast3Target));
-
- SystemFlagsDeployResult result = deployer.deployFlags(archive, false);
-
- assertThat(result.errors()).containsOnly(
- OperationError.listFailed(exception.getMessage(), prodUsWest1Target));
- assertThat(result.flagChanges()).containsOnly(
- FlagDataChange.created(FLAG_ID, prodUsEast3Target, defaultData));
- }
-
- @Test
- void creates_error_entry_for_invalid_flag_archive() {
- FlagsClient flagsClient = mock(FlagsClient.class);
- FlagData defaultData = flagData("flags/my-flag/main.json");
- SystemFlagsDataArchive archive = new SystemFlagsDataArchive.Builder()
- .addFile("main.prod.unknown-region.json", defaultData)
- .build();
- SystemFlagsDeployer deployer = new SystemFlagsDeployer(flagsClient, SYSTEM, Set.of(controllerTarget));
- SystemFlagsDeployResult result = deployer.deployFlags(archive, false);
- assertTrue(result.flagChanges().isEmpty());
- assertThat(result.errors())
- .containsOnly(OperationError.archiveValidationFailed("Unknown flag file: flags/my-flag/main.prod.unknown-region.json"));
- }
-
- @Test
- void creates_error_entry_for_flag_data_of_undefined_flag() {
- FlagData prodUsEast3Data = flagData("flags/my-flag/main.prod.us-east-3.json");
- FlagsClient flagsClient = mock(FlagsClient.class);
- when(flagsClient.listFlagData(prodUsEast3Target))
- .thenReturn(List.of());
- when(flagsClient.listDefinedFlags(prodUsEast3Target))
- .thenReturn(List.of());
- SystemFlagsDataArchive archive = new SystemFlagsDataArchive.Builder()
- .addFile("main.prod.us-east-3.json", prodUsEast3Data)
- .build();
- SystemFlagsDeployer deployer = new SystemFlagsDeployer(flagsClient, SYSTEM, Set.of(prodUsEast3Target));
- SystemFlagsDeployResult result = deployer.deployFlags(archive, true);
- String expectedErrorMessage = "Flag not defined in target zone. If zone/configserver cluster is new, " +
- "add an empty flag data file for this zone as a temporary measure until the stale flag data files are removed.";
- assertThat(result.errors())
- .containsOnly(SystemFlagsDeployResult.OperationError.createFailed(expectedErrorMessage, prodUsEast3Target, prodUsEast3Data));
- }
-
- @Test
- void creates_warning_entry_for_existing_flag_data_for_undefined_flag() {
- FlagData prodUsEast3Data = flagData("flags/my-flag/main.prod.us-east-3.json");
- FlagsClient flagsClient = mock(FlagsClient.class);
- when(flagsClient.listFlagData(prodUsEast3Target))
- .thenReturn(List.of(prodUsEast3Data));
- when(flagsClient.listDefinedFlags(prodUsEast3Target))
- .thenReturn(List.of());
- SystemFlagsDataArchive archive = new SystemFlagsDataArchive.Builder()
- .addFile("main.prod.us-east-3.json", prodUsEast3Data)
- .build();
- SystemFlagsDeployer deployer = new SystemFlagsDeployer(flagsClient, SYSTEM, Set.of(prodUsEast3Target));
- SystemFlagsDeployResult result = deployer.deployFlags(archive, true);
- assertThat(result.errors())
- .containsOnly(OperationError.dataForUndefinedFlag(prodUsEast3Target, new FlagId("my-flag")));
- }
-
- private static FlagData flagData(String filename) {
- return FlagData.deserializeUtf8Json(uncheck(() -> Files.readAllBytes(Paths.get("src/test/resources/system-flags/" + filename))));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java
deleted file mode 100644
index 81db6b02a50..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiOnPremTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.user;
-
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.vespa.athenz.api.AthenzDomain;
-import com.yahoo.vespa.athenz.api.AthenzIdentity;
-import com.yahoo.vespa.athenz.utils.AthenzIdentities;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.jdisc.http.filter.security.misc.User;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.stream.Stream;
-
-/**
- * @author freva
- */
-public class UserApiOnPremTest extends ControllerContainerTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/";
-
- @Test
- void userMetadataOnPremTest() {
- try (Flags.Replacer ignored = Flags.clearFlagsForTesting(PermanentFlags.MAX_TRIAL_TENANTS.id(), PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id())) {
- ContainerTester tester = new ContainerTester(container, responseFiles);
- ControllerTester controller = new ControllerTester(tester);
- User user = new User("dev@domail", "Joe Developer", "dev", null);
-
- controller.createTenant("tenant1", "domain1", 1L);
- controller.createApplication("tenant1", "app1", "default");
- controller.createApplication("tenant1", "app2", "default");
- controller.createApplication("tenant1", "app2", "myinstance");
- controller.createApplication("tenant1", "app3");
-
- controller.createTenant("tenant2", "domain2", 2L);
- controller.createApplication("tenant2", "app2", "test");
-
- controller.createTenant("tenant3", "domain3", 3L);
- controller.createApplication("tenant3", "app1");
-
- controller.createTenant("sandbox", "domain4", 4L);
- controller.createApplication("sandbox", "app1", "default");
- controller.createApplication("sandbox", "app2", "default");
- controller.createApplication("sandbox", "app2", "dev");
-
- AthenzIdentity operator = AthenzIdentities.from("vespa.alice");
- controller.athenzDb().addHostedOperator(operator);
- AthenzIdentity tenantAdmin = AthenzIdentities.from("domain1.bob");
- Stream.of("domain1", "domain2", "domain4")
- .map(AthenzDomain::new)
- .map(controller.athenzDb()::getOrCreateDomain)
- .forEach(d -> d.admin(AthenzIdentities.from("domain1.bob")));
-
- tester.assertResponse(createUserRequest(user, operator),
- new File("on-prem-user-without-applications.json"));
-
- tester.assertResponse(createUserRequest(user, tenantAdmin),
- new File("user-with-applications-athenz.json"));
- }
- }
-
- private Request createUserRequest(User user, AthenzIdentity identity) {
- Request request = new Request("http://localhost:8080/user/v1/user");
- Map<String, String> userAttributes = new HashMap<>();
- userAttributes.put("email", user.email());
- if (user.name() != null)
- userAttributes.put("name", user.name());
- if (user.nickname() != null)
- userAttributes.put("nickname", user.nickname());
- if (user.picture() != null)
- userAttributes.put("picture", user.picture());
- request.getAttributes().put(User.ATTRIBUTE_NAME, Map.copyOf(userAttributes));
- return addIdentityToRequest(request, identity);
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
deleted file mode 100644
index 63ae56e4207..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserApiTest.java
+++ /dev/null
@@ -1,344 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.user;
-
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.MockBillingController;
-import com.yahoo.vespa.hosted.controller.api.integration.billing.PlanId;
-import com.yahoo.jdisc.http.filter.security.misc.User;
-import com.yahoo.vespa.hosted.controller.api.integration.user.UserId;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest;
-import com.yahoo.vespa.hosted.controller.tenant.PendingMailVerification;
-import com.yahoo.vespa.hosted.controller.tenant.Tenant;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.util.List;
-import java.util.Set;
-
-import static com.yahoo.application.container.handler.Request.Method.DELETE;
-import static com.yahoo.application.container.handler.Request.Method.POST;
-import static com.yahoo.application.container.handler.Request.Method.PUT;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author jonmv
- */
-public class UserApiTest extends ControllerContainerCloudTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/";
- private static final String pemPublicKey = "-----BEGIN PUBLIC KEY-----\n" +
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" +
- "z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" +
- "-----END PUBLIC KEY-----\n";
- private static final String otherPemPublicKey = "-----BEGIN PUBLIC KEY-----\n" +
- "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\n" +
- "pDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n" +
- "-----END PUBLIC KEY-----\n";
- private static final String quotedPemPublicKey = pemPublicKey.replaceAll("\\n", "\\\\n");
-
-
- @Test
- void testUserManagement() {
- ContainerTester tester = new ContainerTester(container, responseFiles);
- assertEquals(SystemName.Public, tester.controller().system());
- Set<Role> operator = Set.of(Role.hostedOperator());
- ApplicationId id = ApplicationId.from("my-tenant", "my-app", "default");
-
-
- // GET at application/v4 root fails as it's not public read.
- tester.assertResponse(request("/application/v4/"),
- accessDenied, 403);
-
- // GET at application/v4/tenant succeeds for operators.
- tester.assertResponse(request("/application/v4/tenant")
- .roles(operator),
- "[]");
-
- // POST a tenant is not available to everyone.
- tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
- .data("{\"token\":\"hello\"}"),
- "{\"error-code\":\"FORBIDDEN\",\"message\":\"You are not currently permitted to create tenants. Please contact the Vespa team to request access.\"}", 403);
-
- // POST a tenant is available to operators.
- tester.assertResponse(request("/application/v4/tenant/my-tenant", POST)
- .roles(operator)
- .principal("administrator@tenant")
- .user(new User("administrator@tenant", "administrator", "admin", "picture"))
- .data("{\"token\":\"hello\"}"),
- new File("tenant-without-applications.json"));
-
- // GET at tenant/info with contact information.
- tester.assertResponse(request("/application/v4/tenant/my-tenant/info")
- .roles(operator)
- .principal("administrator@tenant"),
- new File("tenant-info-after-created.json"));
-
- // GET at user/v1 root fails as no access control is defined there.
- tester.assertResponse(request("/user/v1/"),
- accessDenied, 403);
-
- // POST a hosted operator role is not allowed.
- tester.assertResponse(request("/user/v1/tenant/my-tenant", POST)
- .roles(Set.of(Role.administrator(id.tenant())))
- .data("{\"user\":\"evil@evil\",\"roles\":[\"hostedOperator\"]}"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Malformed or illegal role name 'hostedOperator'.\"}", 400);
-
- // POST a tenant developer is available to the tenant owner.
- tester.assertResponse(request("/user/v1/tenant/my-tenant", POST)
- .roles(Set.of(Role.administrator(id.tenant())))
- .data("{\"user\":\"developer@tenant\",\"roles\":[\"developer\",\"reader\"]}"),
- "{\"message\":\"user 'developer@tenant' is now a member of role 'developer' of 'my-tenant', role 'reader' of 'my-tenant'\"}");
-
- // POST a tenant admin is not available to a tenant developer.
- tester.assertResponse(request("/user/v1/tenant/my-tenant", POST)
- .roles(Set.of(Role.developer(id.tenant())))
- .data("{\"user\":\"developer@tenant\",\"roles\":[\"administrator\"]}"),
- accessDenied, 403);
-
- // POST an application is allowed for a tenant developer.
- tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app", POST)
- .principal("developer@tenant")
- .roles(Set.of(Role.developer(id.tenant()))),
- new File("application-created.json"));
-
- // POST an application is not allowed under a different tenant.
- tester.assertResponse(request("/application/v4/tenant/other-tenant/application/my-app", POST)
- .roles(Set.of(Role.administrator(id.tenant()))),
- accessDenied, 403);
-
- // GET tenant role information is available to readers.
- tester.assertResponse(request("/user/v1/tenant/my-tenant")
- .roles(Set.of(Role.reader(id.tenant()))),
- new File("tenant-roles.json"));
-
- // POST a pem deploy key
- tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app/key", POST)
- .roles(Set.of(Role.developer(id.tenant())))
- .data("{\"key\":\"" + pemPublicKey + "\"}"),
- new File("first-deploy-key.json"));
-
- // POST a pem developer key
- tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
- .principal("joe@dev")
- .roles(Set.of(Role.developer(id.tenant())))
- .data("{\"key\":\"" + pemPublicKey + "\"}"),
- new File("first-developer-key.json"));
-
- // POST the same pem developer key for a different user is forbidden
- tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
- .principal("operator@tenant")
- .roles(Set.of(Role.developer(id.tenant())))
- .data("{\"key\":\"" + pemPublicKey + "\"}"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Key " + quotedPemPublicKey + " is already owned by joe@dev\"}",
- 400);
-
- // POST a different developer key for an existing user is forbidden
- tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
- .principal("joe@dev")
- .roles(Set.of(Role.developer(id.tenant())))
- .data("{\"key\":\"" + otherPemPublicKey + "\"}"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"joe@dev is already associated with key " + quotedPemPublicKey + "\"}",
- 400);
-
- // POST in a different pem developer key
- tester.assertResponse(request("/application/v4/tenant/my-tenant/key", POST)
- .principal("developer@tenant")
- .roles(Set.of(Role.developer(id.tenant())))
- .data("{\"key\":\"" + otherPemPublicKey + "\"}"),
- new File("both-developer-keys.json"));
-
- // GET tenant information with keys
- tester.assertResponse(request("/application/v4/tenant/my-tenant/")
- .roles(Set.of(Role.reader(id.tenant()))),
- new File("tenant-with-keys.json"));
-
- // DELETE a pem developer key
- tester.assertResponse(request("/application/v4/tenant/my-tenant/key", DELETE)
- .roles(Set.of(Role.developer(id.tenant())))
- .data("{\"key\":\"" + pemPublicKey + "\"}"),
- new File("second-developer-key.json"));
-
- // PUT in a new secret store for the tenant
- tester.assertResponse(request("/application/v4/tenant/my-tenant/secret-store/secret-foo", PUT)
- .principal("developer@tenant")
- .roles(Set.of(Role.developer(id.tenant())))
- .data("{\"awsId\":\"123\",\"role\":\"secret-role\",\"externalId\":\"abc\"}"),
- "{\"secretStores\":[{\"name\":\"secret-foo\",\"awsId\":\"123\",\"role\":\"secret-role\"}]}",
- 200);
-
- // GET a tenant with secret stores configured
- tester.assertResponse(request("/application/v4/tenant/my-tenant")
- .principal("reader@tenant")
- .roles(Set.of(Role.reader(id.tenant()))),
- new File("tenant-with-secrets.json"));
-
- // DELETE an application is available to developers.
- tester.assertResponse(request("/application/v4/tenant/my-tenant/application/my-app", DELETE)
- .roles(Set.of(Role.developer(id.tenant()))),
- "{\"message\":\"Deleted application my-tenant.my-app\"}");
-
- // DELETE a tenant role is available to tenant admins.
- // DELETE the developer role clears any developer key.
- tester.assertResponse(request("/user/v1/tenant/my-tenant", DELETE)
- .roles(Set.of(Role.administrator(id.tenant())))
- .data("{\"user\":\"developer@tenant\",\"roles\":[\"developer\",\"reader\"]}"),
- "{\"message\":\"user 'developer@tenant' is no longer a member of role 'developer' of 'my-tenant', role 'reader' of 'my-tenant'\"}");
-
- // DELETE the last tenant owner is not allowed.
- tester.assertResponse(request("/user/v1/tenant/my-tenant", DELETE)
- .roles(operator)
- .data("{\"user\":\"administrator@tenant\",\"roles\":[\"administrator\"]}"),
- "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Can't remove the last administrator of a tenant.\"}", 400);
-
- // DELETE the tenant is not allowed
- tester.assertResponse(request("/application/v4/tenant/my-tenant", DELETE)
- .roles(Set.of(Role.developer(id.tenant()))),
- "{\n" +
- " \"code\" : 403,\n" +
- " \"message\" : \"Access denied\"\n" +
- "}", 403);
- }
-
- @Test
- void userMetadataTest() {
- try (Flags.Replacer ignored = Flags.clearFlagsForTesting(PermanentFlags.MAX_TRIAL_TENANTS.id(), PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id())) {
- ContainerTester tester = new ContainerTester(container, responseFiles);
- ((InMemoryFlagSource) tester.controller().flagSource())
- .withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true);
- ControllerTester controller = new ControllerTester(tester);
- Set<Role> operator = Set.of(Role.hostedOperator(), Role.hostedSupporter(), Role.hostedAccountant());
- User user = new User("dev@domail", "Joe Developer", "dev", null);
-
- tester.assertResponse(request("/user/v1/user")
- .roles(operator)
- .user(user),
- new File("user-without-applications.json"));
-
- controller.createTenant("tenant1", Tenant.Type.cloud);
- controller.createApplication("tenant1", "app1", "default");
- controller.createApplication("tenant1", "app2", "default");
- controller.createApplication("tenant1", "app2", "myinstance");
- controller.createApplication("tenant1", "app3");
-
- controller.createTenant("tenant2", Tenant.Type.cloud);
- controller.createApplication("tenant2", "app2", "test");
-
- controller.createTenant("tenant3", Tenant.Type.cloud);
- controller.createApplication("tenant3", "app1");
-
- controller.createTenant("sandbox", Tenant.Type.cloud);
- controller.createApplication("sandbox", "app1", "default");
- controller.createApplication("sandbox", "app2", "default");
- controller.createApplication("sandbox", "app2", "dev");
-
- // Should still be empty because none of the roles explicitly refer to any of the applications
- tester.assertResponse(request("/user/v1/user")
- .roles(operator)
- .user(user),
- new File("user-without-applications.json"));
-
- // Empty applications because tenant dummy does not exist
- tester.assertResponse(request("/user/v1/user")
- .roles(Set.of(Role.administrator(TenantName.from("tenant1")),
- Role.developer(TenantName.from("tenant2")),
- Role.developer(TenantName.from("sandbox")),
- Role.reader(TenantName.from("sandbox"))))
- .user(user),
- new File("user-with-applications-cloud.json"));
- }
- }
-
- @Test
- void findUser() {
- try (Flags.Replacer ignored = Flags.clearFlagsForTesting(PermanentFlags.MAX_TRIAL_TENANTS.id(), PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id())) {
- ContainerTester tester = new ContainerTester(container, responseFiles);
- ((InMemoryFlagSource) tester.controller().flagSource())
- .withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true);
- Set<Role> operator = Set.of(Role.hostedOperator(), Role.hostedSupporter(), Role.hostedAccountant());
- User user = new User("dev@domail", "Joe Developer", "dev", null);
- tester.controller().serviceRegistry().billingController().updateCache(List.of());
-
- Role developer = Role.developer(TenantName.from("scoober"));
- tester.userManagement().createRole(developer);
- tester.userManagement().addToRoles(new UserId("dev@domail"), Set.of(developer));
-
- tester.assertResponse(request("/user/v1/find?email=dev@domail")
- .roles(operator)
- .user(user),
- """
- {"users":[{"isPublic":true,"isCd":false,"hasTrialCapacity":true,"user":{"name":"dev@domail","email":"dev@domail","verified":false},"tenants":{"scoober":{"supported":false,"roles":["developer"]}},"flags":[{"id":"enable-public-signup-flow","rules":[{"value":false}]}]}]}""");
-
- tester.assertResponse(request("/user/v1/find?query=email:dev@domail")
- .roles(operator)
- .user(user),
- """
- {"users":[]}""");
- }
- }
-
- @Test
- void maxTrialTenants() {
- try (Flags.Replacer ignored = Flags.clearFlagsForTesting(PermanentFlags.MAX_TRIAL_TENANTS.id(), PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id())) {
- ContainerTester tester = new ContainerTester(container, responseFiles);
- ((InMemoryFlagSource) tester.controller().flagSource())
- .withIntFlag(PermanentFlags.MAX_TRIAL_TENANTS.id(), 1)
- .withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true);
- ControllerTester controller = new ControllerTester(tester);
- User user = new User("dev@domail", "Joe Developer", "dev", null);
-
- controller.createTenant("tenant1", Tenant.Type.cloud);
- ((MockBillingController) controller.serviceRegistry().billingController()).setTenants(controller.controller().tenants().asList().stream().map(Tenant::name).toList());
-
- tester.assertResponse(
- request("/user/v1/user").user(user),
- new File("user-without-trial-capacity-cloud.json"));
- }
- }
-
- @Test
- void supportTenant() {
- try (Flags.Replacer ignored = Flags.clearFlagsForTesting(PermanentFlags.MAX_TRIAL_TENANTS.id(), PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id())) {
- ContainerTester tester = new ContainerTester(container, responseFiles);
- ((InMemoryFlagSource) tester.controller().flagSource())
- .withIntFlag(PermanentFlags.MAX_TRIAL_TENANTS.id(), 10)
- .withBooleanFlag(PermanentFlags.ENABLE_PUBLIC_SIGNUP_FLOW.id(), true);
- ControllerTester controller = new ControllerTester(tester);
- User user = new User("dev@domail", "Joe Developer", "dev", null);
-
- var tenant1 = controller.createTenant("tenant1", Tenant.Type.cloud);
- var tenant2 = controller.createTenant("tenant2", Tenant.Type.cloud);
- controller.serviceRegistry().billingController().setPlan(tenant2, PlanId.from("paid"), false, false);
-
- tester.assertResponse(
- request("/user/v1/user")
- .roles(Role.reader(tenant1), Role.reader(tenant2))
- .user(user),
- new File("user-with-supported-tenant.json"));
- }
-
- }
-
- @Test
- public void verifyMail() {
- var tester = new ContainerTester(container, responseFiles);
- var controller = new ControllerTester(tester);
- controller.createTenant("tenant1", Tenant.Type.cloud);
- var pendingMailVerification = tester.controller().mailVerifier().sendMailVerification(TenantName.from("tenant1"), "foo@bar.com", PendingMailVerification.MailType.NOTIFICATIONS);
-
- tester.assertResponse(request("/user/v1/email/verify", POST)
- .data("{\"verificationCode\":\"blurb\"}"),
- "{\"error-code\":\"NOT_FOUND\",\"message\":\"No pending email verification with code blurb found\"}", 404);
-
- tester.assertResponse(request("/user/v1/email/verify", POST)
- .data("{\"verificationCode\":\"" + pendingMailVerification.getVerificationCode() + "\"}"),
- "{\"message\":\"Email with verification code " + pendingMailVerification.getVerificationCode() + " has been verified\"}", 200);
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializerTest.java
deleted file mode 100644
index d0a374dbb3a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/UserFlagsSerializerTest.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.user;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.slime.Slime;
-import com.yahoo.slime.SlimeUtils;
-import com.yahoo.test.json.JsonTestHelper;
-import com.yahoo.vespa.flags.FetchVector;
-import com.yahoo.vespa.flags.FlagId;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.flags.JsonNodeRawFlag;
-import com.yahoo.vespa.flags.json.Condition;
-import com.yahoo.vespa.flags.json.FlagData;
-import com.yahoo.vespa.flags.json.Rule;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.vespa.flags.FetchVector.Dimension.INSTANCE_ID;
-import static com.yahoo.vespa.flags.FetchVector.Dimension.CONSOLE_USER_EMAIL;
-import static com.yahoo.vespa.flags.FetchVector.Dimension.TENANT_ID;
-
-/**
- * @author freva
- */
-public class UserFlagsSerializerTest {
-
- @Test
- void user_flag_test() throws IOException {
- String email1 = "alice@domain.tld";
- String email2 = "bob@domain.tld";
-
- try (Flags.Replacer ignored = Flags.clearFlagsForTesting()) {
- Flags.defineStringFlag("string-id", "default value", List.of("owner"), "1970-01-01", "2100-01-01", "desc", "mod", CONSOLE_USER_EMAIL);
- Flags.defineIntFlag("int-id", 123, List.of("owner"), "1970-01-01", "2100-01-01", "desc", "mod", CONSOLE_USER_EMAIL, TENANT_ID, INSTANCE_ID);
- Flags.defineDoubleFlag("double-id", 3.14d, List.of("owner"), "1970-01-01", "2100-01-01", "desc", "mod");
- Flags.defineListFlag("list-id", List.of("a"), String.class, List.of("owner"), "1970-01-01", "2100-01-01", "desc", "mod", CONSOLE_USER_EMAIL);
- Flags.defineJacksonFlag("jackson-id", new ExampleJacksonClass(123, "abc"), ExampleJacksonClass.class,
- List.of("owner"), "1970-01-01", "2100-01-01", "desc", "mod", CONSOLE_USER_EMAIL, TENANT_ID);
-
- Map<FlagId, FlagData> flagData = Stream.of(
- flagData("string-id", rule("\"value1\"", condition(CONSOLE_USER_EMAIL, Condition.Type.WHITELIST, email1))),
- flagData("int-id", rule("456")),
- flagData("list-id",
- rule("[\"value1\"]", condition(CONSOLE_USER_EMAIL, Condition.Type.WHITELIST, email1), condition(INSTANCE_ID, Condition.Type.BLACKLIST, "tenant1:video:default", "tenant1:video:default", "tenant2:music:default")),
- rule("[\"value2\"]", condition(CONSOLE_USER_EMAIL, Condition.Type.WHITELIST, email2)),
- rule("[\"value1\",\"value3\"]", condition(INSTANCE_ID, Condition.Type.BLACKLIST, "tenant1:video:default", "tenant1:video:default", "tenant2:music:default"))),
- flagData("jackson-id", rule("{\"integer\":456,\"string\":\"xyz\"}", condition(CONSOLE_USER_EMAIL, Condition.Type.WHITELIST, email1), condition(TENANT_ID, Condition.Type.WHITELIST, "tenant1", "tenant3")))
- ).collect(Collectors.toMap(FlagData::id, fd -> fd));
-
- // double-id is not here as it does not have CONSOLE_USER_EMAIL dimension
- assertUserFlags("{\"flags\":[" +
- "{\"id\":\"int-id\",\"rules\":[{\"value\":456}]}," + // Default from DB
- "{\"id\":\"jackson-id\",\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"tenant\"}],\"value\":{\"integer\":456,\"string\":\"xyz\"}},{\"value\":{\"integer\":123,\"string\":\"abc\"}}]}," + // Resolved for email
- // Resolved for email, but conditions are empty since this user is not authorized for any tenants
- "{\"id\":\"list-id\",\"rules\":[{\"conditions\":[{\"type\":\"blacklist\",\"dimension\":\"instance\"}],\"value\":[\"value1\"]},{\"conditions\":[{\"type\":\"blacklist\",\"dimension\":\"instance\"}],\"value\":[\"value1\",\"value3\"]},{\"value\":[\"a\"]}]}," +
- "{\"id\":\"string-id\",\"rules\":[{\"value\":\"value1\"}]}]}", // resolved for email
- flagData, Set.of(), false, email1);
-
- // Same as the first one, but user is authorized for tenant1
- assertUserFlags("{\"flags\":[" +
- "{\"id\":\"int-id\",\"rules\":[{\"value\":456}]}," + // Default from DB
- "{\"id\":\"jackson-id\",\"rules\":[{\"conditions\":[{\"type\":\"whitelist\",\"dimension\":\"tenant\",\"values\":[\"tenant1\"]}],\"value\":{\"integer\":456,\"string\":\"xyz\"}},{\"value\":{\"integer\":123,\"string\":\"abc\"}}]}," + // Resolved for email
- // Resolved for email, but conditions have filtered out tenant2
- "{\"id\":\"list-id\",\"rules\":[{\"conditions\":[{\"type\":\"blacklist\",\"dimension\":\"instance\",\"values\":[\"tenant1:video:default\",\"tenant1:video:default\"]}],\"value\":[\"value1\"]},{\"conditions\":[{\"type\":\"blacklist\",\"dimension\":\"instance\",\"values\":[\"tenant1:video:default\",\"tenant1:video:default\"]}],\"value\":[\"value1\",\"value3\"]},{\"value\":[\"a\"]}]}," +
- "{\"id\":\"string-id\",\"rules\":[{\"value\":\"value1\"}]}]}", // resolved for email
- flagData, Set.of("tenant1"), false, email1);
-
- // As operator no conditions are filtered, but the email precondition is applied
- assertUserFlags("{\"flags\":[" +
- "{\"id\":\"int-id\",\"rules\":[{\"value\":456}]}," + // Default from DB
- "{\"id\":\"jackson-id\",\"rules\":[{\"value\":{\"integer\":123,\"string\":\"abc\"}}]}," + // Default from code, no DB values match
- // Includes last value from DB which is not conditioned on email and the default from code
- "{\"id\":\"list-id\",\"rules\":[{\"conditions\":[{\"type\":\"blacklist\",\"dimension\":\"instance\",\"values\":[\"tenant1:video:default\",\"tenant1:video:default\",\"tenant2:music:default\"]}],\"value\":[\"value1\",\"value3\"]},{\"value\":[\"a\"]}]}," +
- "{\"id\":\"string-id\",\"rules\":[{\"value\":\"default value\"}]}]}", // Default from code
- flagData, Set.of(), true, "operator@domain.tld");
- }
- }
-
- private static FlagData flagData(String id, Rule... rules) {
- return new FlagData(new FlagId(id), new FetchVector(), rules);
- }
-
- private static Rule rule(String data, Condition... conditions) {
- return new Rule(Optional.ofNullable(data).map(JsonNodeRawFlag::fromJson), conditions);
- }
-
- private static Condition condition(FetchVector.Dimension dimension, Condition.Type type, String... values) {
- return new Condition.CreateParams(dimension).withValues(values).createAs(type);
- }
-
- private static void assertUserFlags(String expected, Map<FlagId, FlagData> rawFlagData,
- Set<String> authorizedForTenantNames, boolean isOperator, String userEmail) throws IOException {
- Slime slime = new Slime();
- UserFlagsSerializer.toSlime(slime.setObject(), rawFlagData, authorizedForTenantNames.stream().map(TenantName::from).collect(Collectors.toSet()), isOperator, userEmail);
- JsonTestHelper.assertJsonEquals(expected,
- new String(SlimeUtils.toJsonBytes(slime), StandardCharsets.UTF_8));
- }
-
- @JsonIgnoreProperties(ignoreUnknown = true)
- private static class ExampleJacksonClass {
- @JsonProperty("integer") public final int integer;
- @JsonProperty("string") public final String string;
- private ExampleJacksonClass(@JsonProperty("integer") int integer, @JsonProperty("string") String string) {
- this.integer = integer;
- this.string = string;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- ExampleJacksonClass that = (ExampleJacksonClass) o;
- return integer == that.integer &&
- Objects.equals(string, that.string);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(integer, string);
- }
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-created.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-created.json
deleted file mode 100644
index 31bdb07b26b..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/application-created.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "tenant": "my-tenant",
- "application": "my-app",
- "url": "http://localhost:8080/application/v4/tenant/my-tenant/application/my-app"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json
deleted file mode 100644
index bc49135a1db..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/both-developer-keys.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "keys": [
- {
- "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n",
- "user": "joe@dev"
- },
- {
- "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
- "user": "developer@tenant"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json
deleted file mode 100644
index 1c86877b77d..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-deploy-key.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "keys": [
- "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n"
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json
deleted file mode 100644
index dffb0c90df1..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/first-developer-key.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "keys": [
- {
- "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n",
- "user": "joe@dev"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/on-prem-user-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/on-prem-user-without-applications.json
deleted file mode 100644
index 405ed9627c3..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/on-prem-user-without-applications.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "isPublic": false,
- "isCd": false,
- "hasTrialCapacity": true,
- "user": {
- "name": "Joe Developer",
- "email": "dev@domail",
- "nickname": "dev",
- "verified": false
- },
- "tenants": { },
- "operator": [
- "hostedOperator",
- "hostedSupporter",
- "hostedAccountant"
- ],
- "flags": [
- {
- "id": "enable-public-signup-flow",
- "rules": [
- {
- "value": false
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json
deleted file mode 100644
index 64098a775a1..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/second-developer-key.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "keys": [
- {
- "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
- "user": "developer@tenant"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json
deleted file mode 100644
index 1926dcc9f82..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-info-after-created.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "name": "",
- "email": "",
- "website": "",
- "contactName": "administrator",
- "contactEmail": "administrator@tenant",
- "contactEmailVerified": true,
- "contacts": [
- {
- "audiences": [
- "tenant",
- "notifications"
- ],
- "email": "administrator@tenant",
- "emailVerified": true
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-roles.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-roles.json
deleted file mode 100644
index 5aca5de95fc..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-roles.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
- "tenant": "my-tenant",
- "roleNames": [
- "administrator",
- "developer",
- "reader"
- ],
- "users": [
- {
- "name": "administrator@tenant",
- "email": "administrator@tenant",
- "verified": false,
- "roles": {
- "administrator": {
- "explicit": true,
- "implied": false
- },
- "developer": {
- "explicit": true,
- "implied": false
- },
- "reader": {
- "explicit": true,
- "implied": false
- }
- }
- },
- {
- "name": "developer@tenant",
- "email": "developer@tenant",
- "verified": false,
- "roles": {
- "administrator": {
- "explicit": false,
- "implied": false
- },
- "developer": {
- "explicit": true,
- "implied": false
- },
- "reader": {
- "explicit": true,
- "implied": false
- }
- }
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
deleted file mode 100644
index 6b78bfda6c4..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-keys.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
- "tenant": "my-tenant",
- "type": "CLOUD",
- "creator": "administrator@tenant",
- "pemDeveloperKeys": [
- {
- "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\nz/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n-----END PUBLIC KEY-----\n",
- "user": "joe@dev"
- },
- {
- "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
- "user": "developer@tenant"
- }
- ],
- "secretStores": [ ],
- "integrations": {
- "aws": {
- "tenantRole": "my-tenant-tenant-role",
- "accounts": [ ]
- }
- },
- "quota": {
- "budgetUsed": 0.0
- },
- "archiveAccess": { },
- "applications": [
- {
- "tenant": "my-tenant",
- "application": "my-app",
- "url": "http://localhost:8080/application/v4/tenant/my-tenant/application/my-app"
- }
- ],
- "metaData": {
- "createdAtMillis": 1600000000000
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json
deleted file mode 100644
index 8c3bcc041d6..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-with-secrets.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "tenant": "my-tenant",
- "type": "CLOUD",
- "creator": "administrator@tenant",
- "pemDeveloperKeys": [
- {
- "key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEFELzPyinTfQ/sZnTmRp5E4Ve/sbE\npDhJeqczkyFcT2PysJ5sZwm7rKPEeXDOhzTPCyRvbUqc2SGdWbKUGGa/Yw==\n-----END PUBLIC KEY-----\n",
- "user": "developer@tenant"
- }
- ],
- "secretStores": [
- {
- "name": "secret-foo",
- "awsId": "123",
- "role": "secret-role"
- }
- ],
- "integrations": {
- "aws": {
- "tenantRole": "my-tenant-tenant-role",
- "accounts": [
- {
- "name": "secret-foo",
- "awsId": "123",
- "role": "secret-role"
- }
- ]
- }
- },
- "quota": {
- "budgetUsed": 0.0
- },
- "archiveAccess": { },
- "applications": [
- {
- "tenant": "my-tenant",
- "application": "my-app",
- "url": "http://localhost:8080/application/v4/tenant/my-tenant/application/my-app"
- }
- ],
- "metaData": {
- "createdAtMillis": 1600000000000
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
deleted file mode 100644
index 694e886a876..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/tenant-without-applications.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "tenant": "my-tenant",
- "type": "CLOUD",
- "creator": "administrator@tenant",
- "pemDeveloperKeys": [ ],
- "secretStores": [ ],
- "integrations": {
- "aws": {
- "tenantRole": "my-tenant-tenant-role",
- "accounts": [ ]
- }
- },
- "quota": {
- "budgetUsed": 0.0
- },
- "archiveAccess": { },
- "applications": [ ],
- "metaData": {
- "createdAtMillis": 1600000000000
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json
deleted file mode 100644
index 14f78e14c5b..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-athenz.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "isPublic": false,
- "isCd": false,
- "hasTrialCapacity": true,
- "user": {
- "name": "Joe Developer",
- "email": "dev@domail",
- "nickname": "dev",
- "verified": false
- },
- "tenants": {
- "sandbox": {
- "supported": false,
- "roles": [
- "administrator",
- "developer",
- "reader"
- ]
- },
- "tenant1": {
- "supported": false,
- "roles": [
- "administrator",
- "developer",
- "reader"
- ]
- },
- "tenant2": {
- "supported": false,
- "roles": [
- "administrator",
- "developer",
- "reader"
- ]
- }
- },
- "flags": [
- {
- "id": "enable-public-signup-flow",
- "rules": [
- {
- "value": false
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json
deleted file mode 100644
index 39bdc7cd275..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-applications-cloud.json
+++ /dev/null
@@ -1,42 +0,0 @@
-{
- "isPublic": true,
- "isCd": false,
- "hasTrialCapacity": true,
- "user": {
- "name": "Joe Developer",
- "email": "dev@domail",
- "nickname": "dev",
- "verified": false
- },
- "tenants": {
- "sandbox": {
- "supported": false,
- "roles": [
- "developer",
- "reader"
- ]
- },
- "tenant1": {
- "supported": false,
- "roles": [
- "administrator"
- ]
- },
- "tenant2": {
- "supported": false,
- "roles": [
- "developer"
- ]
- }
- },
- "flags": [
- {
- "id": "enable-public-signup-flow",
- "rules": [
- {
- "value": false
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-supported-tenant.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-supported-tenant.json
deleted file mode 100644
index 5ae67f19382..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-with-supported-tenant.json
+++ /dev/null
@@ -1,35 +0,0 @@
-{
- "isPublic": true,
- "isCd": false,
- "hasTrialCapacity": true,
- "user": {
- "name": "Joe Developer",
- "email": "dev@domail",
- "nickname": "dev",
- "verified": false
- },
- "tenants": {
- "tenant1": {
- "supported": false,
- "roles": [
- "reader"
- ]
- },
- "tenant2": {
- "supported": true,
- "roles": [
- "reader"
- ]
- }
- },
- "flags": [
- {
- "id": "enable-public-signup-flow",
- "rules": [
- {
- "value": false
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json
deleted file mode 100644
index 3ad692d9768..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-applications.json
+++ /dev/null
@@ -1,27 +0,0 @@
-{
- "isPublic": true,
- "isCd": false,
- "hasTrialCapacity": true,
- "user": {
- "name": "Joe Developer",
- "email": "dev@domail",
- "nickname": "dev",
- "verified": false
- },
- "tenants": { },
- "operator": [
- "hostedOperator",
- "hostedSupporter",
- "hostedAccountant"
- ],
- "flags": [
- {
- "id": "enable-public-signup-flow",
- "rules": [
- {
- "value": false
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json
deleted file mode 100644
index ef1305647e1..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/user/responses/user-without-trial-capacity-cloud.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "isPublic": true,
- "isCd": false,
- "hasTrialCapacity": false,
- "user": {
- "name": "Joe Developer",
- "email": "dev@domail",
- "nickname": "dev",
- "verified": false
- },
- "tenants": { },
- "flags": [
- {
- "id": "enable-public-signup-flow",
- "rules": [
- {
- "value": false
- }
- ]
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java
deleted file mode 100644
index 183e9968878..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/ZoneApiTest.java
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.zone.v1;
-
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerCloudTest;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.util.List;
-import java.util.Set;
-
-/**
- * @author mpolden
- */
-public class ZoneApiTest extends ControllerContainerCloudTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/";
- private static final List<ZoneApi> zones = List.of(
- ZoneApiMock.fromId("prod.us-north-1"),
- ZoneApiMock.fromId("dev.us-north-2"),
- ZoneApiMock.fromId("test.us-north-3"),
- ZoneApiMock.fromId("staging.us-north-4"));
-
- private static final Set<Role> everyone = Set.of(Role.everyone());
-
- private ContainerTester tester;
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, responseFiles);
- tester.serviceRegistry().zoneRegistry()
- .setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2"))
- .setZones(zones);
- }
-
- @Test
- void test_requests() {
- // GET /zone/v1
- tester.assertResponse(request("/zone/v1")
- .roles(everyone),
- new File("root.json"));
-
- // GET /zone/v1/environment/prod
- tester.assertResponse(request("/zone/v1/environment/prod")
- .roles(everyone),
- new File("prod.json"));
-
- // GET /zone/v1/environment/dev/default
- tester.assertResponse(request("/zone/v1/environment/dev/default")
- .roles(everyone),
- new File("default-for-region.json"));
- }
-
- @Test
- void test_invalid_requests() {
- // GET /zone/v1/environment/prod/default: No default region
- tester.assertResponse(request("/zone/v1/environment/prod/default")
- .roles(everyone),
- new File("no-default-region.json"),
- 400);
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/default-for-region.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/default-for-region.json
deleted file mode 100644
index 99a9b3f6f7f..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/default-for-region.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "name": "us-north-2",
- "url": "http://localhost:8080/zone/v2/dev/us-north-2"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/no-default-region.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/no-default-region.json
deleted file mode 100644
index bdc6601a2e9..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/no-default-region.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "error-code": "BAD_REQUEST",
- "message": "No default region for environment: prod"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/prod.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/prod.json
deleted file mode 100644
index 24350f9dbd4..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/prod.json
+++ /dev/null
@@ -1,6 +0,0 @@
-[
- {
- "name": "us-north-1",
- "url": "http://localhost:8080/zone/v2/prod/us-north-1"
- }
-]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/root.json
deleted file mode 100644
index 2121d121c44..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v1/responses/root.json
+++ /dev/null
@@ -1,18 +0,0 @@
-[
- {
- "name": "dev",
- "url": "http://localhost:8080/zone/v1/environment/dev"
- },
- {
- "name": "prod",
- "url": "http://localhost:8080/zone/v1/environment/prod"
- },
- {
- "name": "staging",
- "url": "http://localhost:8080/zone/v1/environment/staging"
- },
- {
- "name": "test",
- "url": "http://localhost:8080/zone/v1/environment/test"
- }
-]
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java
deleted file mode 100644
index b680a4341f3..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/ZoneApiTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.restapi.zone.v2;
-
-import com.yahoo.application.container.handler.Request.Method;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.hosted.controller.integration.ConfigServerProxyMock;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.controller.proxy.ProxyRequest;
-import com.yahoo.vespa.hosted.controller.restapi.ContainerTester;
-import com.yahoo.vespa.hosted.controller.restapi.ControllerContainerTest;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.File;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author mpolden
- */
-public class ZoneApiTest extends ControllerContainerTest {
-
- private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/";
- private static final List<ZoneApi> zones = List.of(
- ZoneApiMock.fromId("prod.us-north-1"),
- ZoneApiMock.fromId("dev.aws-us-north-2"),
- ZoneApiMock.fromId("test.us-north-3"),
- ZoneApiMock.fromId("staging.us-north-4"));
-
- private ContainerTester tester;
- private ConfigServerProxyMock proxy;
-
- @BeforeEach
- public void before() {
- tester = new ContainerTester(container, responseFiles);
- tester.serviceRegistry().zoneRegistry()
- .setDefaultRegionForEnvironment(Environment.dev, RegionName.from("us-north-2"))
- .setZones(zones);
- proxy = (ConfigServerProxyMock) container.components().getComponent(ConfigServerProxyMock.class.getName());
- }
-
- @Test
- void test_requests() {
- // GET /zone/v2
- tester.assertResponse(authenticatedRequest("http://localhost:8080/zone/v2"),
- new File("root.json"));
-
- // GET /zone/v2/prod/us-north-1
- tester.assertResponse(authenticatedRequest("http://localhost:8080/zone/v2/prod/us-north-1"),
- "ok");
-
- assertLastRequest(ZoneId.from("prod", "us-north-1"), 1, "GET");
-
- // GET /zone/v2/nodes/v2/node/?recursive=true
- tester.assertResponse(authenticatedRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/?recursive=true"),
- "ok");
- assertLastRequest(ZoneId.from("prod", "us-north-1"), 1, "GET");
-
- // POST /zone/v2/dev/us-north-2/nodes/v2/command/restart?hostname=node1
- tester.assertResponse(operatorRequest("http://localhost:8080/zone/v2/dev/aws-us-north-2/nodes/v2/command/restart?hostname=node1",
- "", Method.POST),
- "ok");
-
- // PUT /zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1
- tester.assertResponse(operatorRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/state/dirty/node1",
- "", Method.PUT), "ok");
- assertLastRequest(ZoneId.from("prod", "us-north-1"), 1, "PUT");
-
- // DELETE /zone/v2/prod/us-north-1/nodes/v2/node/node1
- tester.assertResponse(operatorRequest("http://localhost:8080/zone/v2/prod/us-north-1/nodes/v2/node/node1",
- "", Method.DELETE), "ok");
- assertLastRequest(ZoneId.from("prod", "us-north-1"), 1, "DELETE");
-
- // PATCH /zone/v2/prod/us-north-1/nodes/v2/node/node1
- tester.assertResponse(operatorRequest("http://localhost:8080/zone/v2/dev/aws-us-north-2/nodes/v2/node/node1",
- "{\"currentRestartGeneration\": 1}",
- Method.PATCH), "ok");
- assertLastRequest(ZoneId.from("dev", "aws-us-north-2"), 1, "PATCH");
- assertEquals("{\"currentRestartGeneration\": 1}", proxy.lastRequestBody().get());
-
- assertFalse(tester.controller().auditLogger().readLog().entries().isEmpty(), "Actions are logged to audit log");
- }
-
- @Test
- void test_invalid_requests() {
- // POST /zone/v2/prod/us-north-34/nodes/v2
- tester.assertResponse(operatorRequest("http://localhost:8080/zone/v2/prod/us-north-42/nodes/v2",
- "", Method.POST),
- new File("unknown-zone.json"), 400);
- assertFalse(proxy.lastReceived().isPresent());
- }
-
- private void assertLastRequest(ZoneId zoneId, int targets, String method) {
- ProxyRequest last = proxy.lastReceived().orElseThrow();
- assertEquals(targets, last.getTargets().size());
- assertTrue(last.getTargets().get(0).toString().contains(zoneId.value()));
- assertEquals(com.yahoo.jdisc.http.HttpRequest.Method.valueOf(method), last.getMethod());
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json
deleted file mode 100644
index bd1bc40ba81..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/root.json
+++ /dev/null
@@ -1,26 +0,0 @@
-{
- "uris": [
- "http://localhost:8080/zone/v2/prod/us-north-1",
- "http://localhost:8080/zone/v2/dev/aws-us-north-2",
- "http://localhost:8080/zone/v2/test/us-north-3",
- "http://localhost:8080/zone/v2/staging/us-north-4"
- ],
- "zones": [
- {
- "environment": "prod",
- "region": "us-north-1"
- },
- {
- "environment": "dev",
- "region": "aws-us-north-2"
- },
- {
- "environment": "test",
- "region": "us-north-3"
- },
- {
- "environment": "staging",
- "region": "us-north-4"
- }
- ]
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json
deleted file mode 100644
index c7d6e4b8400..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/zone/v2/responses/unknown-zone.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "error-code": "BAD_REQUEST",
- "message": "No such zone: prod.us-north-42"
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java
deleted file mode 100644
index a671f567895..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/RoutingPoliciesTest.java
+++ /dev/null
@@ -1,1683 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing;
-
-import ai.vespa.http.DomainName;
-import com.google.common.collect.ImmutableMap;
-import com.yahoo.config.application.api.DeploymentSpec;
-import com.yahoo.config.application.api.ValidationId;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.AthenzDomain;
-import com.yahoo.config.provision.AthenzService;
-import com.yahoo.config.provision.CloudName;
-import com.yahoo.config.provision.ClusterSpec;
-import com.yahoo.config.provision.Environment;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.InstanceName;
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.AuthMethod;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.config.provision.zone.ZoneId;
-import com.yahoo.vespa.flags.Flags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.Instance;
-import com.yahoo.vespa.hosted.controller.api.identifiers.DeploymentId;
-import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCertificate;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.ContainerEndpoint;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.LoadBalancer;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.Record.Type;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName;
-import com.yahoo.vespa.hosted.controller.api.integration.dns.VpcEndpointService.ChallengeState;
-import com.yahoo.vespa.hosted.controller.application.Endpoint;
-import com.yahoo.vespa.hosted.controller.application.EndpointId;
-import com.yahoo.vespa.hosted.controller.application.EndpointList;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.TenantAndApplicationId;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.certificate.UnassignedCertificate;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.dns.NameServiceQueue;
-import com.yahoo.vespa.hosted.controller.dns.RemoveRecords;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.Timeout;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assertions.fail;
-
-/**
- * @author mortent
- * @author mpolden
- */
-public class RoutingPoliciesTest {
-
- private static final ZoneApiMock zoneApi1 = ZoneApiMock.newBuilder()
- .with(ZoneId.from("prod", "aws-us-west-11a"))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("us-west-11")
- .build();
- private static final ZoneApiMock zoneApi2 = ZoneApiMock.newBuilder().with(ZoneId.from("prod", "aws-us-central-22a"))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("us-central-22")
- .build();
- private static final ZoneApiMock zoneApi3 = ZoneApiMock.newBuilder().with(ZoneId.from("prod", "aws-us-east-33a"))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("us-east-33")
- .build();
- private static final ZoneApiMock zoneApi4 = ZoneApiMock.newBuilder()
- .with(ZoneId.from("prod", "aws-us-east-33b"))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("us-east-33")
- .build();
- private static final ZoneApiMock zoneApi5 = ZoneApiMock.newBuilder()
- .with(ZoneId.from("prod", "aws-us-north-44a"))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("north-44")
- .build();
- private static final ZoneApiMock zoneApi6 = ZoneApiMock.newBuilder()
- .with(ZoneId.from("prod", "aws-us-south-55a"))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("south-55")
- .build();
-
- private static final ZoneId zone1 = zoneApi1.getId();
- private static final ZoneId zone2 = zoneApi2.getId();
- private static final ZoneId zone3 = zoneApi3.getId();
- private static final ZoneId zone4 = zoneApi4.getId();
- private static final ZoneId zone5 = zoneApi5.getId();
- private static final ZoneId zone6 = zoneApi6.getId();
-
- private static final ZoneId testZonePublic = ZoneId.from("test", "aws-us-east-2c");
- private static final ZoneId stagingZonePublic = ZoneId.from("staging", "aws-us-east-3c");
- private static final ZoneId testZoneMain = ZoneId.from("test", "us-east-1");
- private static final ZoneId stagingZoneMain = ZoneId.from("staging", "us-east-3");
-
- private static final ApplicationPackage applicationPackage = applicationPackageBuilder().region(zone1.region())
- .region(zone2.region())
- .build();
-
- @Test
- void global_routing_policies() {
- var tester = new RoutingPoliciesTester();
- var context1 = tester.newDeploymentContext("tenant1", "app1", "default");
- var context2 = tester.newDeploymentContext("tenant1", "app2", "default");
- int clustersPerZone = 2;
- int numberOfDeployments = 2;
- var applicationPackage = applicationPackageBuilder()
- .region(zone1.region())
- .region(zone2.region())
- .endpoint("r0", "c0")
- .endpoint("r1", "c0", zone1.region().value())
- .endpoint("r2", "c1")
- .build();
- tester.provisionLoadBalancers(clustersPerZone, context1.instanceId(), zone1, zone2);
-
- // Creates alias records
- context1.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- tester.assertTargets(context1.instanceId(), EndpointId.of("r0"), 0, zone1, zone2);
- tester.assertTargets(context1.instanceId(), EndpointId.of("r1"), 0, zone1);
- tester.assertTargets(context1.instanceId(), EndpointId.of("r2"), 1, zone1, zone2);
- assertEquals(numberOfDeployments * clustersPerZone,
- tester.policiesOf(context1.instance().id()).size(),
- "Routing policy count is equal to cluster count");
- assertEquals(List.of(),
- tester.controllerTester().controller().routing()
- .readDeclaredEndpointsOf(context1.instanceId())
- .scope(Endpoint.Scope.zone)
- .legacy()
- .asList(),
- "No endpoints marked as legacy");
-
- // Applications gains a new deployment
- ApplicationPackage applicationPackage2 = applicationPackageBuilder()
- .region(zone1.region())
- .region(zone2.region())
- .region(zone3.region())
- .endpoint("r0", "c0")
- .endpoint("r1", "c0", zone1.region().value())
- .endpoint("r2", "c1")
- .build();
- numberOfDeployments++;
- tester.provisionLoadBalancers(clustersPerZone, context1.instanceId(), zone3);
- context1.submit(applicationPackage2).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
-
- // Endpoints are updated to contain cluster in new deployment
- tester.assertTargets(context1.instanceId(), EndpointId.of("r0"), 0, zone1, zone2, zone3);
- tester.assertTargets(context1.instanceId(), EndpointId.of("r1"), 0, zone1);
- tester.assertTargets(context1.instanceId(), EndpointId.of("r2"), 1, zone1, zone2, zone3);
-
- // Ensure test deployment only updates endpoints of which it is a member
- context1.submit(applicationPackage2)
- .runJob(DeploymentContext.systemTest);
- NameServiceQueue queue = tester.controllerTester().controller().curator().readNameServiceQueue();
- assertEquals(List.of(new RemoveRecords(Optional.of(TenantAndApplicationId.from(context1.instanceId())),
- Record.Type.CNAME,
- RecordName.from("app1.tenant1.us-east-1.test.vespa.oath.cloud"))),
- queue.requests());
- context1.completeRollout();
-
- // Another application is deployed with a single cluster and global endpoint
- var endpoint4 = "r0.app2.tenant1.global.vespa.oath.cloud";
- tester.provisionLoadBalancers(1, context2.instanceId(), zone1, zone2);
- var applicationPackage3 = applicationPackageBuilder()
- .region(zone1.region())
- .region(zone2.region())
- .endpoint("r0", "c0")
- .build();
- context2.submit(applicationPackage3).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- tester.assertTargets(context2.instanceId(), EndpointId.of("r0"), 0, zone1, zone2);
-
- // A deployment of app2 is removed
- var applicationPackage4 = applicationPackageBuilder()
- .region(zone1.region())
- .endpoint("r0", "c0")
- .allow(ValidationId.globalEndpointChange)
- .allow(ValidationId.deploymentRemoval)
- .build();
- context2.submit(applicationPackage4).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- tester.assertTargets(context2.instanceId(), EndpointId.of("r0"), 0, zone1);
- assertEquals(1, tester.policiesOf(context2.instanceId()).size());
-
- // All global endpoints for app1 are removed
- ApplicationPackage applicationPackage5 = applicationPackageBuilder()
- .region(zone1.region())
- .region(zone2.region())
- .region(zone3.region())
- .allow(ValidationId.globalEndpointChange)
- .build();
- context1.submit(applicationPackage5).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- tester.assertTargets(context1.instanceId(), EndpointId.of("r0"), 0);
- tester.assertTargets(context1.instanceId(), EndpointId.of("r1"), 0);
- tester.assertTargets(context1.instanceId(), EndpointId.of("r2"), 0);
- var policies = tester.policiesOf(context1.instanceId());
- assertEquals(clustersPerZone * numberOfDeployments, policies.size());
- assertTrue(policies.asList().stream().allMatch(policy -> policy.instanceEndpoints().isEmpty()),
- "Rotation membership is removed from all policies");
- assertEquals(1, tester.aliasDataOf(endpoint4).size(), "Rotations for " + context2.application() + " are not removed");
- assertEquals(List.of("c0.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c0.app1.tenant1.aws-us-east-33a.vespa.oath.cloud",
- "c0.app1.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c0.app2.tenant1.aws-us-west-11-w.vespa.oath.cloud",
- "c0.app2.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-east-33a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "r0.app2.tenant1.global.vespa.oath.cloud"),
- tester.recordNames(),
- "Endpoints in DNS matches current config");
- }
-
- @Test
- void global_routing_policies_with_duplicate_region() {
- var tester = new RoutingPoliciesTester();
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- int clustersPerZone = 2;
- int numberOfDeployments = 3;
- var applicationPackage = applicationPackageBuilder()
- .region(zone1.region())
- .region(zone3.region())
- .region(zone4.region())
- .endpoint("r0", "c0")
- .endpoint("r1", "c1")
- .build();
- tester.provisionLoadBalancers(clustersPerZone, context.instanceId(), zone1, zone3, zone4);
-
- // Creates alias records
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1, zone3, zone4);
- tester.assertTargets(context.instanceId(), EndpointId.of("r1"), 1, zone1, zone3, zone4);
- assertEquals(numberOfDeployments * clustersPerZone,
- tester.policiesOf(context.instance().id()).size(),
- "Routing policy count is equal to cluster count");
-
- // A zone in shared region is set out
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone4), RoutingStatus.Value.out,
- RoutingStatus.Agent.tenant);
- context.flushDnsUpdates();
-
- // Weight of inactive zone is set to zero
- ApplicationId application2 = context.instanceId();
- EndpointId endpointId2 = EndpointId.of("r0");
- Map<ZoneId, Long> zoneWeights1 = ImmutableMap.of(zone1, 1L,
- zone3, 1L,
- zone4, 0L);
- tester.assertTargets(application2, endpointId2, ClusterSpec.Id.from("c0"), 0, zoneWeights1);
-
- // Other zone in shared region is set out. Entire record group for the region is removed as all zones in the
- // region are out (weight sum = 0)
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone3), RoutingStatus.Value.out,
- RoutingStatus.Agent.tenant);
- context.flushDnsUpdates();
- ApplicationId application1 = context.instanceId();
- EndpointId endpointId1 = EndpointId.of("r0");
- tester.assertTargets(application1, endpointId1, ClusterSpec.Id.from("c0"), 0, ImmutableMap.of(zone1, 1L));
-
- // Everything is set back in
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone3), RoutingStatus.Value.in,
- RoutingStatus.Agent.tenant);
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone4), RoutingStatus.Value.in,
- RoutingStatus.Agent.tenant);
- context.flushDnsUpdates();
- ApplicationId application = context.instanceId();
- EndpointId endpointId = EndpointId.of("r0");
- Map<ZoneId, Long> zoneWeights = ImmutableMap.of(zone1, 1L,
- zone3, 1L,
- zone4, 1L);
- tester.assertTargets(application, endpointId, ClusterSpec.Id.from("c0"), 0, zoneWeights);
- }
-
- @Test
- void zone_routing_policies() {
- zone_routing_policies(false);
- zone_routing_policies(true);
- }
-
- private void zone_routing_policies(boolean sharedRoutingLayer) {
- var tester = new RoutingPoliciesTester();
- var context1 = tester.newDeploymentContext("tenant1", "app1", "default");
- var context2 = tester.newDeploymentContext("tenant1", "app2", "default");
-
- // Deploy application
- int clustersPerZone = 2;
- tester.provisionLoadBalancers(clustersPerZone, context1.instanceId(), sharedRoutingLayer, zone1, zone2);
- context1.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
-
- // Deployment creates records and policies for all clusters in all zones
- List<String> expectedRecords = List.of(
- "c0.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c0.app1.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-west-11a.vespa.oath.cloud"
- );
- assertEquals(expectedRecords, tester.recordNames());
- assertEquals(4, tester.policiesOf(context1.instanceId()).size());
- assertEquals(List.of(),
- tester.controllerTester().controller().routing()
- .readEndpointsOf(context1.deploymentIdIn(zone1))
- .scope(Endpoint.Scope.zone)
- .legacy()
- .asList(),
- "No endpoints marked as legacy");
-
- // Next deploy does nothing
- context1.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- assertEquals(expectedRecords, tester.recordNames());
- assertEquals(4, tester.policiesOf(context1.instanceId()).size());
-
- // Add 1 cluster in each zone and deploy
- tester.provisionLoadBalancers(clustersPerZone + 1, context1.instanceId(), sharedRoutingLayer, zone1, zone2);
- context1.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- expectedRecords = List.of(
- "c0.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c0.app1.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c2.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c2.app1.tenant1.aws-us-west-11a.vespa.oath.cloud"
- );
- assertEquals(expectedRecords, tester.recordNames());
- assertEquals(6, tester.policiesOf(context1.instanceId()).size());
-
- // Deploy another application
- tester.provisionLoadBalancers(clustersPerZone, context2.instanceId(), sharedRoutingLayer, zone1, zone2);
- context2.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- expectedRecords = List.of(
- "c0.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c0.app1.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c0.app2.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c0.app2.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c1.app2.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c1.app2.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c2.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c2.app1.tenant1.aws-us-west-11a.vespa.oath.cloud"
- );
- assertEquals(expectedRecords.stream().sorted().toList(), tester.recordNames().stream().sorted().toList());
- assertEquals(4, tester.policiesOf(context2.instanceId()).size());
-
- // Deploy removes cluster from app1
- tester.provisionLoadBalancers(clustersPerZone, context1.instanceId(), sharedRoutingLayer, zone1, zone2);
- context1.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- expectedRecords = List.of(
- "c0.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c0.app1.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c0.app2.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c0.app2.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c1.app2.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c1.app2.tenant1.aws-us-west-11a.vespa.oath.cloud"
- );
- assertEquals(expectedRecords, tester.recordNames());
-
- // Remove app2 completely
- tester.controllerTester().controller().applications().requireInstance(context2.instanceId()).deployments().keySet()
- .forEach(zone -> tester.controllerTester().controller().applications().deactivate(context2.instanceId(), zone));
- context2.flushDnsUpdates();
- expectedRecords = List.of(
- "c0.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c0.app1.tenant1.aws-us-west-11a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-central-22a.vespa.oath.cloud",
- "c1.app1.tenant1.aws-us-west-11a.vespa.oath.cloud"
- );
- assertEquals(expectedRecords, tester.recordNames());
- assertTrue(tester.routingPolicies().read(context2.instanceId()).isEmpty(), "Removes stale routing policies " + context2.application());
- assertEquals(4, tester.routingPolicies().read(context1.instanceId()).size(), "Keeps routing policies for " + context1.application());
- }
-
- @Test
- void zone_routing_policies_with_shared_routing() {
- var tester = new RoutingPoliciesTester(new DeploymentTester(), false);
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- tester.provisionLoadBalancers(1, context.instanceId(), true, zone1, zone2);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- assertEquals(0, tester.controllerTester().controller().curator().readNameServiceQueue().requests().size());
- // Ordinary endpoints are not created in DNS
- assertEquals(List.of(), tester.recordNames());
- assertEquals(2, tester.policiesOf(context.instanceId()).size());
- }
-
- @Test
- void zone_routing_policies_with_shared_routing_and_generated_endpoint_config_and_token() {
- var tester = new RoutingPoliciesTester(new DeploymentTester(), false);
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- tester.setEndpointConfig(EndpointConfig.generated);
- addCertificateToPool("cafed00d", UnassignedCertificate.State.ready, tester);
- ApplicationPackage applicationPackage = applicationPackageBuilder().region(zone1.region())
- .region(zone2.region())
- .container("c0", AuthMethod.mtls, AuthMethod.token)
- .build();
- tester.provisionLoadBalancers(1, context.instanceId(), true,
- testZoneMain, stagingZoneMain, zone1, zone2);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- // This only creates wildcard endpoint names in DNS because legacy names in shared routing-mode use a static
- // wildcard DNS record pointing to the routing layer
- assertEquals(List.of("a9c8c045.cafed00d.z.vespa.oath.cloud",
- "dc5e383c.cafed00d.z.vespa.oath.cloud",
- "ebd395b6.cafed00d.z.vespa.oath.cloud",
- "ee82b867.cafed00d.z.vespa.oath.cloud"),
- tester.recordNames());
- }
-
- @Test
- void global_routing_policies_in_rotationless_system() {
- var tester = new RoutingPoliciesTester(SystemName.Public);
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- List<ZoneId> prodZones = tester.controllerTester().controller().zoneRegistry().zones().all().in(Environment.prod).ids();
- ZoneId zone1 = prodZones.get(0);
- ZoneId zone2 = prodZones.get(1);
- tester.provisionLoadBalancers(1, context.instanceId(), zone1, zone2);
-
- var applicationPackage = applicationPackageBuilder()
- .region(zone1.region().value())
- .endpoint("r0", "c0")
- .trustDefaultCertificate()
- .build();
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
-
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1);
- assertTrue(context.application().instances().values().stream()
- .map(Instance::rotations)
- .allMatch(List::isEmpty), "No rotations assigned");
- }
-
- @Test
- void cross_cloud_policies() {
- var tester = new RoutingPoliciesTester(SystemName.Public);
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- var zone1 = ZoneId.from("prod", "aws-us-east-1c");
- var zone2 = ZoneId.from("prod", "gcp-us-south1-b");
- tester.provisionLoadBalancers(1, context.instanceId(), zone1, zone2);
-
- var applicationPackage = applicationPackageBuilder()
- .region(zone1.region().value())
- .region(zone2.region().value())
- .endpoint("r0", "c0")
- .trustDefaultCertificate()
- .build();
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
-
- List<String> expectedRecords = List.of(
- "c0.app1.tenant1.aws-us-east-1.w.vespa-app.cloud",
- "c0.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud",
- "c0.app1.tenant1.gcp-us-south1-b.z.vespa-app.cloud",
- "c0.app1.tenant1.gcp-us-south1.w.vespa-app.cloud",
- "r0.app1.tenant1.g.vespa-app.cloud"
- );
- assertEquals(expectedRecords, tester.recordNames());
-
- assertEquals(List.of("lb-0--tenant1.app1.default--prod.aws-us-east-1c."), tester.recordDataOf(Record.Type.CNAME, expectedRecords.get(1)));
- assertEquals(List.of("10.0.0.0"), tester.recordDataOf(Record.Type.A, expectedRecords.get(2)));
- assertEquals(List.of("weighted/10.0.0.0/prod.gcp-us-south1-b/1"), tester.recordDataOf(Record.Type.DIRECT, expectedRecords.get(3)));
- assertEquals(List.of("latency/c0.app1.tenant1.aws-us-east-1.w.vespa-app.cloud/dns-zone-1/prod.aws-us-east-1c",
- "latency/c0.app1.tenant1.gcp-us-south1.w.vespa-app.cloud/ignored/prod.gcp-us-south1-b"),
- tester.recordDataOf(Record.Type.ALIAS, expectedRecords.get(4)));
-
- // Application is removed and records are cleaned up
- tester.controllerTester().controller().applications().requireInstance(context.instanceId()).deployments().keySet()
- .forEach(zone -> tester.controllerTester().controller().applications().deactivate(context.instanceId(), zone));
- context.flushDnsUpdates();
- assertEquals(List.of(), tester.recordNames());
- }
-
- @Test
- void global_routing_policies_in_public() {
- var tester = new RoutingPoliciesTester(SystemName.Public);
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- List<ZoneId> prodZones = tester.controllerTester().controller().zoneRegistry().zones().all().in(Environment.prod).ids();
- ZoneId zone1 = prodZones.get(0);
- ZoneId zone2 = prodZones.get(1);
-
- var applicationPackage = applicationPackageBuilder()
- .region(zone1.region())
- .region(zone2.region())
- .endpoint("default", "default")
- .trustDefaultCertificate()
- .build();
- context.submit(applicationPackage).deploy();
-
- tester.assertTargets(context.instanceId(), EndpointId.defaultId(),
- ClusterSpec.Id.from("default"), 0,
- Map.of(zone1, 1L, zone2, 1L));
- assertEquals(List.of("app1.tenant1.aws-eu-west-1.w.vespa-app.cloud",
- "app1.tenant1.aws-eu-west-1a.z.vespa-app.cloud",
- "app1.tenant1.aws-us-east-1.w.vespa-app.cloud",
- "app1.tenant1.aws-us-east-1c.z.vespa-app.cloud",
- "app1.tenant1.g.vespa-app.cloud"
- ),
- tester.recordNames(),
- "Registers expected DNS names");
- }
-
- @Test
- void manual_deployment_creates_routing_policy() {
- // Empty application package is valid in manually deployed environments
- var tester = new RoutingPoliciesTester();
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- var emptyApplicationPackage = new ApplicationPackageBuilder().build();
- var zone = ZoneId.from("dev", "us-east-1");
- var zoneApi = ZoneApiMock.from(zone.environment(), zone.region());
- tester.controllerTester().serviceRegistry().zoneRegistry()
- .setZones(zoneApi)
- .exclusiveRoutingIn(zoneApi);
-
- // Deploy to dev
- context.runJob(zone, emptyApplicationPackage);
- assertEquals(DeploymentSpec.empty, context.application().deploymentSpec(), "DeploymentSpec is not persisted");
- context.flushDnsUpdates();
-
- // Routing policy is created and DNS is updated
- assertEquals(1, tester.policiesOf(context.instanceId()).size());
- assertEquals(List.of("app1.tenant1.us-east-1.dev.vespa.oath.cloud"), tester.recordNames());
- }
-
- @Test
- void manual_deployment_creates_routing_policy_with_non_empty_spec() {
- // Initial deployment
- var tester = new RoutingPoliciesTester();
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- context.submit(applicationPackage).deploy();
- var zone = ZoneId.from("dev", "us-east-1");
- tester.controllerTester().setRoutingMethod(List.of(zone), RoutingMethod.exclusive);
- var prodRecords = List.of("app1.tenant1.aws-us-central-22a.vespa.oath.cloud", "app1.tenant1.aws-us-west-11a.vespa.oath.cloud");
- assertEquals(prodRecords, tester.recordNames());
-
- // Deploy to dev under different instance
- var devContext = tester.newDeploymentContext(context.application().id().instance("user"));
- devContext.runJob(zone, applicationPackage);
-
- assertEquals(applicationPackage.deploymentSpec(), context.application().deploymentSpec(), "DeploymentSpec is persisted");
- context.flushDnsUpdates();
-
- // Routing policy is created and DNS is updated
- assertEquals(1, tester.policiesOf(devContext.instanceId()).size());
- assertEquals(Stream.concat(prodRecords.stream(), Stream.of("user.app1.tenant1.us-east-1.dev.vespa.oath.cloud")).sorted().toList(),
- tester.recordNames());
- }
-
- @Test
- void reprovisioning_load_balancer_preserves_cname_record() {
- var tester = new RoutingPoliciesTester();
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
-
- // Initial load balancer is provisioned
- tester.provisionLoadBalancers(1, context.instanceId(), zone1);
- var applicationPackage = applicationPackageBuilder()
- .region(zone1.region())
- .build();
-
- // Application is deployed
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- var expectedRecords = List.of(
- "c0.app1.tenant1.aws-us-west-11a.vespa.oath.cloud"
- );
- assertEquals(expectedRecords, tester.recordNames());
- assertEquals(1, tester.policiesOf(context.instanceId()).size());
-
- // Application is removed and the load balancer is deprovisioned
- tester.controllerTester().controller().applications().deactivate(context.instanceId(), zone1);
- tester.controllerTester().configServer().removeLoadBalancers(context.instanceId(), zone1);
-
- // Load balancer for the same application is provisioned again, but with a different hostname
- var newHostname = HostName.of("new-hostname");
- var loadBalancer = new LoadBalancer("LB-0-Z-" + zone1.value(),
- context.instanceId(),
- ClusterSpec.Id.from("c0"),
- Optional.of(newHostname),
- Optional.empty(),
- LoadBalancer.State.active,
- Optional.of("dns-zone-1"),
- Optional.empty(),
- Optional.empty(),
- true);
- tester.controllerTester().configServer().putLoadBalancers(zone1, List.of(loadBalancer));
-
- // Application redeployment preserves DNS record
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- assertEquals(expectedRecords, tester.recordNames());
- assertEquals(1, tester.policiesOf(context.instanceId()).size());
- assertEquals(newHostname.value() + ".",
- tester.recordDataOf(Record.Type.CNAME, expectedRecords.iterator().next()).get(0),
- "CNAME points to current load balancer");
- }
-
- @Test
- @Timeout(30)
- void private_dns_for_vpc_endpoint() {
- // Challenge answered for endpoint
- RoutingPoliciesTester tester = new RoutingPoliciesTester();
- tester.tester.controllerTester().serviceRegistry().vpcEndpointService().enabled.set(true);
-
- DeploymentContext app = tester.newDeploymentContext("t", "a", "default");
- ApplicationPackage appPackage = applicationPackageBuilder().region(zone3.region()).build();
- app.submit(appPackage);
-
- app.deploy();
-
- // TXT records are cleaned up when deployments are deactivated.
- // The last challenge is the last to go here, and we must flush it ourselves.
- assertEquals(List.of("a.t.aws-us-east-33a.vespa.oath.cloud",
- "challenge--a.t.aws-us-east-33a.vespa.oath.cloud"),
- tester.recordNames());
- app.flushDnsUpdates();
- assertEquals(Set.of(new Record(Type.CNAME,
- RecordName.from("a.t.aws-us-east-33a.vespa.oath.cloud"),
- RecordData.from("lb-0--t.a.default--prod.aws-us-east-33a.")),
- new Record(Type.TXT,
- RecordName.from("challenge--a.t.aws-us-east-33a.vespa.oath.cloud"),
- RecordData.from("system"))),
- tester.controllerTester().nameService().records());
-
- tester.controllerTester().controller().applications().deactivate(app.instanceId(), zone3);
- app.flushDnsUpdates();
- assertEquals(Set.of(),
- tester.controllerTester().nameService().records());
-
- // Deployment fails because challenge is not answered (immediately).
- tester.tester.controllerTester().serviceRegistry().vpcEndpointService().outcomes
- .put(RecordName.from("challenge--a.t.aws-us-east-33a.vespa.oath.cloud"), ChallengeState.running);
- assertEquals("Status of run 2 of production-aws-us-east-33a for t.a ==> expected: <succeeded> but was: <unfinished>",
- assertThrows(AssertionError.class,
- () -> app.submit(appPackage).deploy())
- .getMessage());
- }
-
- @Test
- void set_global_endpoint_status() {
- var tester = new RoutingPoliciesTester();
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
-
- // Provision load balancers and deploy application
- tester.provisionLoadBalancers(1, context.instanceId(), zone1, zone2);
- var applicationPackage = applicationPackageBuilder()
- .region(zone1.region())
- .region(zone2.region())
- .endpoint("r0", "c0", zone1.region().value(), zone2.region().value())
- .endpoint("r1", "c0", zone1.region().value(), zone2.region().value())
- .build();
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
-
- // Global DNS record is created
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1, zone2);
- tester.assertTargets(context.instanceId(), EndpointId.of("r1"), 0, zone1, zone2);
-
- // Global routing status is overridden in one zone
- var changedAt = tester.controllerTester().clock().instant();
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone1), RoutingStatus.Value.out,
- RoutingStatus.Agent.tenant);
- context.flushDnsUpdates();
-
- // Inactive zone is removed from global DNS record
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone2);
- tester.assertTargets(context.instanceId(), EndpointId.of("r1"), 0, zone2);
-
- // Status details is stored in policy
- var policy1 = tester.routingPolicies().read(context.deploymentIdIn(zone1)).first().get();
- assertEquals(RoutingStatus.Value.out, policy1.routingStatus().value());
- assertEquals(RoutingStatus.Agent.tenant, policy1.routingStatus().agent());
- assertEquals(changedAt.truncatedTo(ChronoUnit.MILLIS), policy1.routingStatus().changedAt());
-
- // Other zone remains in
- var policy2 = tester.routingPolicies().read(context.deploymentIdIn(zone2)).first().get();
- assertEquals(RoutingStatus.Value.in, policy2.routingStatus().value());
- assertEquals(RoutingStatus.Agent.system, policy2.routingStatus().agent());
- assertEquals(Instant.EPOCH, policy2.routingStatus().changedAt());
-
- // Next deployment does not affect status
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- context.flushDnsUpdates();
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone2);
- tester.assertTargets(context.instanceId(), EndpointId.of("r1"), 0, zone2);
-
- // Deployment is set back in
- tester.controllerTester().clock().advance(Duration.ofHours(1));
- changedAt = tester.controllerTester().clock().instant();
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone1), RoutingStatus.Value.in, RoutingStatus.Agent.tenant);
- context.flushDnsUpdates();
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1, zone2);
- tester.assertTargets(context.instanceId(), EndpointId.of("r1"), 0, zone1, zone2);
-
- policy1 = tester.routingPolicies().read(context.deploymentIdIn(zone1)).first().get();
- assertEquals(RoutingStatus.Value.in, policy1.routingStatus().value());
- assertEquals(RoutingStatus.Agent.tenant, policy1.routingStatus().agent());
- assertEquals(changedAt.truncatedTo(ChronoUnit.MILLIS), policy1.routingStatus().changedAt());
- }
-
- @Test
- void set_zone_global_endpoint_status() {
- var tester = new RoutingPoliciesTester();
- var context1 = tester.newDeploymentContext("tenant1", "app1", "default");
- var context2 = tester.newDeploymentContext("tenant2", "app2", "default");
- var contexts = List.of(context1, context2);
-
- // Deploy applications
- var applicationPackage = applicationPackageBuilder()
- .region(zone1.region())
- .region(zone2.region())
- .endpoint("default", "c0", zone1.region().value(), zone2.region().value())
- .build();
- for (var context : contexts) {
- tester.provisionLoadBalancers(1, context.instanceId(), zone1, zone2);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- tester.assertTargets(context.instanceId(), EndpointId.defaultId(), 0, zone1, zone2);
- }
-
- // Set zone out
- tester.routingPolicies().setRoutingStatus(zone2, RoutingStatus.Value.out);
- context1.flushDnsUpdates();
- tester.assertTargets(context1.instanceId(), EndpointId.defaultId(), 0, zone1);
- tester.assertTargets(context2.instanceId(), EndpointId.defaultId(), 0, zone1);
- for (var context : contexts) {
- var policies = tester.routingPolicies().read(context.instanceId());
- assertTrue(policies.asList().stream()
- .map(RoutingPolicy::routingStatus)
- .map(RoutingStatus::value)
- .allMatch(status -> status == RoutingStatus.Value.in),
- "Global routing status for policy remains " + RoutingStatus.Value.in);
- }
- var changedAt = tester.controllerTester().clock().instant();
- var zonePolicy = tester.controllerTester().controller().curator().readZoneRoutingPolicy(zone2);
- assertEquals(RoutingStatus.Value.out, zonePolicy.routingStatus().value());
- assertEquals(RoutingStatus.Agent.operator, zonePolicy.routingStatus().agent());
- assertEquals(changedAt.truncatedTo(ChronoUnit.MILLIS), zonePolicy.routingStatus().changedAt());
-
- // Setting status per deployment does not affect status as entire zone is out
- tester.routingPolicies().setRoutingStatus(context1.deploymentIdIn(zone2), RoutingStatus.Value.in, RoutingStatus.Agent.tenant);
- context1.flushDnsUpdates();
- tester.assertTargets(context1.instanceId(), EndpointId.defaultId(), 0, zone1);
- tester.assertTargets(context2.instanceId(), EndpointId.defaultId(), 0, zone1);
-
- // Set single deployment out
- tester.routingPolicies().setRoutingStatus(context1.deploymentIdIn(zone2), RoutingStatus.Value.out, RoutingStatus.Agent.tenant);
- context1.flushDnsUpdates();
-
- // Set zone back in. Deployment set explicitly out, remains out, the rest are in
- tester.routingPolicies().setRoutingStatus(zone2, RoutingStatus.Value.in);
- context1.flushDnsUpdates();
- tester.assertTargets(context1.instanceId(), EndpointId.defaultId(), 0, zone1);
- tester.assertTargets(context2.instanceId(), EndpointId.defaultId(), 0, zone1, zone2);
- }
-
- @Test
- void non_production_deployment_is_not_registered_in_global_endpoint() {
- var tester = new RoutingPoliciesTester(SystemName.Public);
-
- // Configure the system to use the same region for test, staging and prod
- var context = tester.tester.newDeploymentContext();
- var endpointId = EndpointId.of("r0");
- var applicationPackage = applicationPackageBuilder()
- .trustDefaultCertificate()
- .region("aws-us-east-1c")
- .endpoint(endpointId.id(), "default")
- .build();
-
- // Application starts deployment
- List<JobType> testJobs = tester.controllerTester().zoneRegistry().zones().all()
- .in(Environment.test, Environment.staging)
- .in(CloudName.AWS)
- .ids()
- .stream()
- .map(JobType::deploymentTo)
- .toList();
- context = context.submit(applicationPackage);
- for (var testJob : testJobs) {
- context = context.runJob(testJob);
- // Since runJob implicitly tears down the deployment and immediately deletes DNS records associated with the
- // deployment, we consume only one DNS update at a time here
- do {
- context.flushDnsUpdates(1);
- tester.assertTargets(context.instanceId(), endpointId, 0);
- } while (!tester.recordNames().isEmpty());
- }
-
- // Deployment completes
- context.completeRollout();
- tester.assertTargets(context.instanceId(), endpointId, ClusterSpec.Id.from("default"), 0, Map.of(ZoneId.from("prod", "aws-us-east-1c"), 1L));
- }
-
- @Test
- void changing_global_routing_status_never_removes_all_members() {
- var tester = new RoutingPoliciesTester();
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
-
- // Provision load balancers and deploy application
- tester.provisionLoadBalancers(1, context.instanceId(), zone1, zone2);
- var applicationPackage = applicationPackageBuilder()
- .region(zone1.region())
- .region(zone2.region())
- .endpoint("r0", "c0", zone1.region().value(), zone2.region().value())
- .build();
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
-
- // Global DNS record is created, pointing to all configured zones
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1, zone2);
-
- // Global routing status is overridden for one deployment
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone1), RoutingStatus.Value.out,
- RoutingStatus.Agent.tenant);
- context.flushDnsUpdates();
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone2);
-
- // Setting remaining deployment out is rejected
- try {
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone2), RoutingStatus.Value.out,
- RoutingStatus.Agent.tenant);
- } catch (IllegalArgumentException e) {
- assertEquals("Cannot deactivate routing for tenant1.app1 in prod.aws-us-central-22a as it's the last remaining active deployment in endpoint https://r0.app1.tenant1.global.vespa.oath.cloud/ [scope=global, legacy=false, routingMethod=exclusive, authMethod=mtls, name=r0]", e.getMessage());
- }
- context.flushDnsUpdates();
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone2);
-
- // Inactive deployment is put back in. Global DNS record now points to all deployments
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone1), RoutingStatus.Value.in,
- RoutingStatus.Agent.tenant);
- context.flushDnsUpdates();
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1, zone2);
-
- // One deployment is deactivated again
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone2), RoutingStatus.Value.out,
- RoutingStatus.Agent.tenant);
- context.flushDnsUpdates();
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1);
-
- // Operator deactivates routing for entire zone where deployment only has that zone activated. This does not
- // change status for the deployment as it's the only one left
- tester.routingPolicies().setRoutingStatus(zone1, RoutingStatus.Value.out);
- context.flushDnsUpdates();
- assertEquals(RoutingStatus.Value.out, tester.routingPolicies().read(zone1).routingStatus().value());
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1);
-
- // Inactive deployment is set in which allows the zone-wide status to take effect
- tester.routingPolicies().setRoutingStatus(context.deploymentIdIn(zone2), RoutingStatus.Value.in,
- RoutingStatus.Agent.tenant);
- context.flushDnsUpdates();
- for (var policy : tester.routingPolicies().read(context.instanceId())) {
- assertSame(RoutingStatus.Value.in, policy.routingStatus().value());
- }
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone2);
-
- // Zone-wide status is changed to in
- tester.routingPolicies().setRoutingStatus(zone1, RoutingStatus.Value.in);
- context.flushDnsUpdates();
- tester.assertTargets(context.instanceId(), EndpointId.of("r0"), 0, zone1, zone2);
- }
-
- @Test
- void application_endpoint_routing_policy() {
- RoutingPoliciesTester tester = new RoutingPoliciesTester();
- TenantAndApplicationId application = TenantAndApplicationId.from("tenant1", "app1");
- ApplicationId betaInstance = application.instance("beta");
- ApplicationId mainInstance = application.instance("main");
-
- DeploymentContext betaContext = tester.newDeploymentContext(betaInstance);
- DeploymentContext mainContext = tester.newDeploymentContext(mainInstance);
- var applicationPackage = applicationPackageBuilder()
- .instances("beta,main")
- .region(zone5.region())
- .region(zone6.region())
- .applicationEndpoint("a0", "c0",
- Map.of(zone5.region().value(), Map.of(betaInstance.instance(), 2,
- mainInstance.instance(), 8),
- zone6.region().value(), Map.of(mainInstance.instance(), 7)))
- .applicationEndpoint("a1", "c1", zone6.region().value(),
- Map.of(betaInstance.instance(), 4,
- mainInstance.instance(), 6))
- .build();
- for (var zone : List.of(zone5, zone6)) {
- tester.provisionLoadBalancers(2, betaInstance, zone);
- tester.provisionLoadBalancers(2, mainInstance, zone);
- }
-
- // Application endpoints are not created until production jobs run
- betaContext.submit(applicationPackage)
- .runJob(DeploymentContext.systemTest);
- assertEquals(List.of("beta.app1.tenant1.us-east-1.test.vespa.oath.cloud"), tester.recordNames());
- betaContext.runJob(DeploymentContext.stagingTest);
- assertEquals(List.of("beta.app1.tenant1.us-east-3.staging.vespa.oath.cloud"), tester.recordNames());
-
- // Deploy both instances
- betaContext.completeRollout();
-
- // Application endpoint points to both instances with correct weights
- DeploymentId betaZone5 = betaContext.deploymentIdIn(zone5);
- DeploymentId mainZone5 = mainContext.deploymentIdIn(zone5);
- DeploymentId betaZone6 = betaContext.deploymentIdIn(zone6);
- DeploymentId mainZone6 = mainContext.deploymentIdIn(zone6);
- tester.assertTargets(application, EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
- Map.of(betaZone5, 2,
- mainZone5, 8,
- mainZone6, 7));
- tester.assertTargets(application, EndpointId.of("a1"), ClusterSpec.Id.from("c1"), 1,
- Map.of(betaZone6, 4,
- mainZone6, 6));
-
- // Weights are updated
- applicationPackage = applicationPackageBuilder()
- .instances("beta,main")
- .region(zone5.region())
- .region(zone6.region())
- .applicationEndpoint("a0", "c0",
- Map.of(zone5.region().value(), Map.of(betaInstance.instance(), 3,
- mainInstance.instance(), 7),
- zone6.region().value(), Map.of(mainInstance.instance(), 2)))
- .applicationEndpoint("a1", "c1", zone6.region().value(),
- Map.of(betaInstance.instance(), 1,
- mainInstance.instance(), 9))
- .build();
- betaContext.submit(applicationPackage).deploy();
- tester.assertTargets(application, EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
- Map.of(betaZone5, 3,
- mainZone5, 7,
- mainZone6, 2));
- tester.assertTargets(application, EndpointId.of("a1"), ClusterSpec.Id.from("c1"), 1,
- Map.of(betaZone6, 1,
- mainZone6, 9));
-
- // An endpoint is removed
- applicationPackage = applicationPackageBuilder()
- .instances("beta,main")
- .region(zone5.region())
- .region(zone6.region())
- .applicationEndpoint("a0", "c0", zone5.region().value(),
- Map.of(betaInstance.instance(), 1))
- .build();
- betaContext.submit(applicationPackage).deploy();
-
- // Application endpoints now point to a single instance
- tester.assertTargets(application, EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
- Map.of(betaZone5, 1));
- assertTrue(tester.controllerTester().controller().routing()
- .readDeclaredEndpointsOf(mainContext.application())
- .named(EndpointId.of("a1"), Endpoint.Scope.application).isEmpty(),
- "Endpoint removed");
- assertEquals(List.of("a0.app1.tenant1.a.vespa.oath.cloud",
- "beta.app1.tenant1.aws-us-north-44a.vespa.oath.cloud",
- "beta.app1.tenant1.aws-us-south-55a.vespa.oath.cloud",
- "c0.beta.app1.tenant1.aws-us-north-44a.vespa.oath.cloud",
- "c0.beta.app1.tenant1.aws-us-south-55a.vespa.oath.cloud",
- "c0.main.app1.tenant1.aws-us-north-44a.vespa.oath.cloud",
- "c0.main.app1.tenant1.aws-us-south-55a.vespa.oath.cloud",
- "c1.beta.app1.tenant1.aws-us-north-44a.vespa.oath.cloud",
- "c1.beta.app1.tenant1.aws-us-south-55a.vespa.oath.cloud",
- "c1.main.app1.tenant1.aws-us-north-44a.vespa.oath.cloud",
- "c1.main.app1.tenant1.aws-us-south-55a.vespa.oath.cloud",
- "main.app1.tenant1.aws-us-north-44a.vespa.oath.cloud",
- "main.app1.tenant1.aws-us-south-55a.vespa.oath.cloud"),
- tester.recordNames(),
- "Endpoints in DNS matches current config");
-
- // Ensure test deployment only updates endpoint of which it is a member
- betaContext.submit(applicationPackage)
- .runJob(DeploymentContext.systemTest);
- NameServiceQueue queue = tester.controllerTester().controller().curator().readNameServiceQueue();
- assertEquals(List.of(new RemoveRecords(Optional.of(TenantAndApplicationId.from(betaContext.instanceId())),
- Record.Type.CNAME,
- RecordName.from("beta.app1.tenant1.us-east-1.test.vespa.oath.cloud"))),
- queue.requests());
- }
-
- @Test
- void application_endpoint_routing_status() {
- RoutingPoliciesTester tester = new RoutingPoliciesTester();
- TenantAndApplicationId application = TenantAndApplicationId.from("tenant1", "app1");
- ApplicationId betaInstance = application.instance("beta");
- ApplicationId mainInstance = application.instance("main");
-
- DeploymentContext betaContext = tester.newDeploymentContext(betaInstance);
- DeploymentContext mainContext = tester.newDeploymentContext(mainInstance);
- var applicationPackage = applicationPackageBuilder()
- .instances("beta,main")
- .region(zone5.region())
- .region(zone6.region())
- .applicationEndpoint("a0", "c0", Map.of(zone5.region().value(), Map.of(betaInstance.instance(), 2,
- mainInstance.instance(), 8),
- zone6.region().value(), Map.of(mainInstance.instance(), 9)))
- .build();
- tester.provisionLoadBalancers(1, betaInstance, zone5);
- tester.provisionLoadBalancers(1, mainInstance, zone5);
- tester.provisionLoadBalancers(1, mainInstance, zone6);
-
- // Deploy both instances
- betaContext.submit(applicationPackage).deploy();
-
- // Application endpoint points to both instances with correct weights
- DeploymentId betaZone1 = betaContext.deploymentIdIn(zone5);
- DeploymentId mainZone1 = mainContext.deploymentIdIn(zone5);
- DeploymentId mainZone2 = mainContext.deploymentIdIn(zone6);
- tester.assertTargets(application, EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
- Map.of(betaZone1, 2,
- mainZone1, 8,
- mainZone2, 9));
-
- // Changing routing status removes deployment from DNS
- tester.routingPolicies().setRoutingStatus(mainZone1, RoutingStatus.Value.out, RoutingStatus.Agent.tenant);
- betaContext.flushDnsUpdates();
- tester.assertTargets(application, EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
- Map.of(betaZone1, 2,
- mainZone2, 9));
-
- // Changing routing status for remaining deployments adds back all deployments, because removing all deployments
- // puts all IN
- tester.routingPolicies().setRoutingStatus(betaZone1, RoutingStatus.Value.out, RoutingStatus.Agent.tenant);
- try {
- tester.routingPolicies().setRoutingStatus(mainZone2, RoutingStatus.Value.out, RoutingStatus.Agent.tenant);
- fail("Expected exception");
- } catch (IllegalArgumentException e) {
- assertEquals("Cannot deactivate routing for tenant1.app1.main in prod.aws-us-south-55a as it's the last remaining active deployment in endpoint https://a0.app1.tenant1.a.vespa.oath.cloud/ [scope=application, legacy=false, routingMethod=exclusive, authMethod=mtls, name=a0]",
- e.getMessage());
- }
-
- // Re-activating one zone allows us to take out another
- tester.routingPolicies().setRoutingStatus(mainZone1, RoutingStatus.Value.in, RoutingStatus.Agent.tenant);
- tester.routingPolicies().setRoutingStatus(mainZone2, RoutingStatus.Value.out, RoutingStatus.Agent.tenant);
- betaContext.flushDnsUpdates();
- tester.assertTargets(application, EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
- Map.of(mainZone1, 8));
-
- // Activate all deployments again
- tester.routingPolicies().setRoutingStatus(betaZone1, RoutingStatus.Value.in, RoutingStatus.Agent.tenant);
- tester.routingPolicies().setRoutingStatus(mainZone2, RoutingStatus.Value.in, RoutingStatus.Agent.tenant);
- betaContext.flushDnsUpdates();
- tester.assertTargets(application, EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
- Map.of(betaZone1, 2,
- mainZone1, 8,
- mainZone2, 9));
- }
-
- @Test
- public void duplicate_endpoint_ids_across_different_scopes() {
- RoutingPoliciesTester tester = new RoutingPoliciesTester();
- ApplicationId instance = ApplicationId.from("t1", "a1", "i1");
- DeploymentContext context = tester.newDeploymentContext(instance);
- var applicationPackage = applicationPackageBuilder()
- .instances(instance.instance().value())
- .region(zone1.region())
- .region(zone2.region())
- .endpoint("default", "c0")
- .applicationEndpoint("default", "c0", zone1.region().value(),
- Map.of(instance.instance(), 1))
- .build();
- tester.provisionLoadBalancers(1, instance, zone1, zone2);
- context.submit(applicationPackage).deploy();
- tester.assertTargets(instance, EndpointId.defaultId(), 0, zone1, zone2);
- tester.assertTargets(TenantAndApplicationId.from(instance), EndpointId.defaultId(),
- ClusterSpec.Id.from("c0"), 0, Map.of(context.deploymentIdIn(zone1), 1));
-
- tester.controllerTester().controller().applications().deactivate(context.instanceId(), zone1);
- tester.controllerTester().controller().applications().deactivate(context.instanceId(), zone2);
- assertTrue(tester.controllerTester().controller().routing().policies().read(context.instanceId()).isEmpty(),
- "Policies removed");
- }
-
- @Test
- public void combined_endpoint_config() {
- var tester = new RoutingPoliciesTester(SystemName.Public).setEndpointConfig(EndpointConfig.combined);
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
-
- // Deploy application
- int clustersPerZone = 2;
- var zone1 = ZoneId.from("prod", "aws-us-east-1c");
- var zone2 = ZoneId.from("prod", "aws-eu-west-1a");
- var zone3 = ZoneId.from("prod", "aws-us-east-1a"); // To test global endpoint pointing to two zones in same cloud-native region
- ApplicationPackage applicationPackage = applicationPackageBuilder().region(zone1.region())
- .region(zone2.region())
- .region(zone3.region())
- .container("c0", AuthMethod.mtls)
- .container("c1", AuthMethod.mtls, AuthMethod.token)
- .endpoint("foo", "c0")
- .applicationEndpoint("bar", "c0", Map.of(zone1.region().value(), Map.of(InstanceName.defaultName(), 1)))
- .build();
- tester.provisionLoadBalancers(clustersPerZone, context.instanceId(), zone1, zone2, zone3);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
-
- // Deployment creates generated zone names
- List<String> expectedRecords = List.of(
- // save me, jebus!
- "a6414896.f5549014.aws-eu-west-1.w.vespa-app.cloud",
- "aa7591aa.f5549014.z.vespa-app.cloud",
- "bar.app1.tenant1.a.vespa-app.cloud",
- "bc50b636.f5549014.z.vespa-app.cloud",
- "c0.app1.tenant1.aws-eu-west-1.w.vespa-app.cloud",
- "c0.app1.tenant1.aws-eu-west-1a.z.vespa-app.cloud",
- "c0.app1.tenant1.aws-us-east-1.w.vespa-app.cloud",
- "c0.app1.tenant1.aws-us-east-1a.z.vespa-app.cloud",
- "c0.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud",
- "c1.app1.tenant1.aws-eu-west-1a.z.vespa-app.cloud",
- "c1.app1.tenant1.aws-us-east-1a.z.vespa-app.cloud",
- "c1.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud",
- "c33db5ed.f5549014.z.vespa-app.cloud",
- "d467800f.f5549014.z.vespa-app.cloud",
- "d71005bf.f5549014.z.vespa-app.cloud",
- "dd0971b4.f5549014.g.vespa-app.cloud",
- "eb48ad53.f5549014.z.vespa-app.cloud",
- "ec1e1288.f5549014.z.vespa-app.cloud",
- "f2fa41ec.f5549014.a.vespa-app.cloud",
- "f411d177.f5549014.z.vespa-app.cloud",
- "f4a4d111.f5549014.z.vespa-app.cloud",
- "fcf1bd63.f5549014.aws-us-east-1.w.vespa-app.cloud",
- "foo.app1.tenant1.g.vespa-app.cloud"
- );
- assertEquals(expectedRecords, tester.recordNames());
- assertEquals(6, tester.policiesOf(context.instanceId()).size());
- ClusterSpec.Id cluster0 = ClusterSpec.Id.from("c0");
- ClusterSpec.Id cluster1 = ClusterSpec.Id.from("c1");
- // The expected number of endpoints are created
- for (var zone : List.of(zone1, zone2)) {
- EndpointList zoneEndpoints = tester.controllerTester().controller().routing()
- .readEndpointsOf(context.deploymentIdIn(zone))
- .scope(Endpoint.Scope.zone);
- EndpointList generated = zoneEndpoints.generated();
- assertEquals(1, generated.cluster(cluster0).size());
- assertEquals(0, generated.cluster(cluster0).authMethod(AuthMethod.token).size());
- assertEquals(2, generated.cluster(cluster1).size());
- assertEquals(1, generated.cluster(cluster1).authMethod(AuthMethod.token).size());
- EndpointList legacy = zoneEndpoints.legacy();
- assertEquals(1, legacy.cluster(cluster0).size());
- assertEquals(0, legacy.cluster(cluster0).authMethod(AuthMethod.token).size());
- assertEquals(1, legacy.cluster(cluster1).size());
- assertEquals(0, legacy.cluster(cluster1).authMethod(AuthMethod.token).size());
- }
- EndpointList declaredEndpoints = tester.controllerTester().controller().routing().readDeclaredEndpointsOf(context.application());
- assertEquals(1, declaredEndpoints.scope(Endpoint.Scope.global).generated().size());
- assertEquals(1, declaredEndpoints.scope(Endpoint.Scope.global).legacy().size());
- assertEquals(1, declaredEndpoints.scope(Endpoint.Scope.application).generated().size());
- assertEquals(1, declaredEndpoints.scope(Endpoint.Scope.application).legacy().size());
- Map<DeploymentId, Set<ContainerEndpoint>> containerEndpointsInProd = tester.containerEndpoints(Environment.prod);
-
- // Ordinary endpoints point to expected targets
- tester.assertTargets(context.instanceId(), EndpointId.of("foo"), cluster0, 0,
- ImmutableMap.of(zone1, 1L,
- zone2, 1L,
- zone3, 1L));
- tester.assertTargets(context.application().id(), EndpointId.of("bar"), cluster0, 0,
- Map.of(context.deploymentIdIn(zone1), 1));
-
- // Generated endpoints point to expected targets
- tester.assertTargets(context.instanceId(), EndpointId.of("foo"), cluster0, 0,
- ImmutableMap.of(zone1, 1L,
- zone2, 1L,
- zone3, 1L),
- true);
- tester.assertTargets(context.application().id(), EndpointId.of("bar"), cluster0, 0,
- Map.of(context.deploymentIdIn(zone1), 1),
- true);
-
- // Next deployment does not change generated names
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- assertEquals(expectedRecords, tester.recordNames());
- assertEquals(containerEndpointsInProd, tester.containerEndpoints(Environment.prod));
-
- // One endpoint is removed
- applicationPackage = applicationPackageBuilder().region(zone1.region())
- .region(zone2.region())
- .region(zone3.region())
- .container("c0", AuthMethod.mtls)
- .container("c1", AuthMethod.mtls, AuthMethod.token)
- .applicationEndpoint("bar", "c0", Map.of(zone1.region().value(), Map.of(InstanceName.defaultName(), 1)))
- .allow(ValidationId.globalEndpointChange)
- .build();
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- assertEquals(List.of(
- "aa7591aa.f5549014.z.vespa-app.cloud",
- "bar.app1.tenant1.a.vespa-app.cloud",
- "bc50b636.f5549014.z.vespa-app.cloud",
- "c0.app1.tenant1.aws-eu-west-1a.z.vespa-app.cloud",
- "c0.app1.tenant1.aws-us-east-1a.z.vespa-app.cloud",
- "c0.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud",
- "c1.app1.tenant1.aws-eu-west-1a.z.vespa-app.cloud",
- "c1.app1.tenant1.aws-us-east-1a.z.vespa-app.cloud",
- "c1.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud",
- "c33db5ed.f5549014.z.vespa-app.cloud",
- "d467800f.f5549014.z.vespa-app.cloud",
- "d71005bf.f5549014.z.vespa-app.cloud",
- "eb48ad53.f5549014.z.vespa-app.cloud",
- "ec1e1288.f5549014.z.vespa-app.cloud",
- "f2fa41ec.f5549014.a.vespa-app.cloud",
- "f411d177.f5549014.z.vespa-app.cloud",
- "f4a4d111.f5549014.z.vespa-app.cloud"
- ), tester.recordNames());
-
- // Removing application removes all records
- context.submit(ApplicationPackageBuilder.fromDeploymentXml("<deployment version='1.0'/>",
- ValidationId.deploymentRemoval,
- ValidationId.globalEndpointChange));
- context.flushDnsUpdates();
- assertEquals(List.of(), tester.recordNames());
- }
-
- @Test
- public void generated_endpoint_config_with_token() {
- var tester = new RoutingPoliciesTester(SystemName.Public).setEndpointConfig(EndpointConfig.generated);
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- addCertificateToPool("cafed00d", UnassignedCertificate.State.ready, tester);
-
- // Deploy application without token
- var zone1 = ZoneId.from("prod", "aws-us-east-1c");
- ApplicationPackage applicationPackage = applicationPackageBuilder().region(zone1.region())
- .container("c0", AuthMethod.mtls)
- .endpoint("foo", "c0")
- .build();
- tester.provisionLoadBalancers(1, context.instanceId(), testZonePublic, stagingZonePublic, zone1);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.test, Environment.staging, Environment.prod).deploy();
- assertEquals(List.of("a9c8c045.cafed00d.g.vespa-app.cloud",
- "ebd395b6.cafed00d.z.vespa-app.cloud",
- "fcf1bd63.cafed00d.aws-us-east-1.w.vespa-app.cloud"),
- tester.recordNames());
-
- // Re-deploy with token enabled
- applicationPackage = applicationPackageBuilder().region(zone1.region())
- .container("c0", AuthMethod.mtls, AuthMethod.token)
- .endpoint("foo", "c0")
- .build();
- tester.provisionLoadBalancers(1, context.instanceId(), testZonePublic, stagingZonePublic);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.test, Environment.staging, Environment.prod).deploy();
- // Additional zone- and global-scoped endpoints are added (token)
- assertEquals(List.of("a9c8c045.cafed00d.g.vespa-app.cloud",
- "b7e79800.cafed00d.z.vespa-app.cloud",
- "c60d3149.cafed00d.g.vespa-app.cloud",
- "ebd395b6.cafed00d.z.vespa-app.cloud",
- "fcf1bd63.cafed00d.aws-us-east-1.w.vespa-app.cloud"),
- tester.recordNames());
-
- // Add new endpoint is generated for an additional global endpoint
- applicationPackage = applicationPackageBuilder().region(zone1.region())
- .container("c0", AuthMethod.mtls, AuthMethod.token)
- .endpoint("foo", "c0")
- .endpoint("bar", "c0")
- .build();
- tester.provisionLoadBalancers(1, context.instanceId(), testZonePublic, stagingZonePublic);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.test, Environment.staging, Environment.prod).deploy();
- List<String> expectedRecords = List.of("a9c8c045.cafed00d.g.vespa-app.cloud",
- "aa7591aa.cafed00d.g.vespa-app.cloud",
- "b7e79800.cafed00d.z.vespa-app.cloud",
- "c60d3149.cafed00d.g.vespa-app.cloud",
- "d467800f.cafed00d.g.vespa-app.cloud",
- "ebd395b6.cafed00d.z.vespa-app.cloud",
- "fcf1bd63.cafed00d.aws-us-east-1.w.vespa-app.cloud");
- assertEquals(expectedRecords, tester.recordNames());
-
- // No change on redeployment
- tester.provisionLoadBalancers(1, context.instanceId(), testZonePublic, stagingZonePublic);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.test, Environment.staging, Environment.prod).deploy();
- assertEquals(expectedRecords, tester.recordNames());
- }
-
- @Test
- public void generated_endpoint_config() {
- var tester = new RoutingPoliciesTester(SystemName.Public).setEndpointConfig(EndpointConfig.generated);
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
- addCertificateToPool("cafed00d", UnassignedCertificate.State.ready, tester);
-
- // Deploy application
- var zone1 = ZoneId.from("prod", "aws-us-east-1c");
- var zone2 = ZoneId.from("prod", "aws-eu-west-1a");
- ApplicationPackage applicationPackage = applicationPackageBuilder().region(zone1.region())
- .container("c0", AuthMethod.mtls)
- .endpoint("foo", "c0")
- .build();
- tester.provisionLoadBalancers(1, context.instanceId(), zone1);
- // ConfigServerMock provisions a load balancer for the "default" cluster, but in this scenario we need full
- // control over the load balancer name because "default" has no special treatment when using generated endpoints
- tester.provisionLoadBalancers(1, context.instanceId(), testZonePublic, stagingZonePublic);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.test, Environment.staging, Environment.prod).deploy();
- tester.assertTargets(context.instance().id(), EndpointId.of("foo"), ClusterSpec.Id.from("c0"),
- 0, Map.of(zone1, 1L), true);
- assertEquals(List.of("a9c8c045.cafed00d.g.vespa-app.cloud",
- "ebd395b6.cafed00d.z.vespa-app.cloud",
- "fcf1bd63.cafed00d.aws-us-east-1.w.vespa-app.cloud"),
- tester.recordNames());
-
- // Another zone is added to global endpoint
- applicationPackage = applicationPackageBuilder().region(zone1.region())
- .region(zone2.region())
- .container("c0", AuthMethod.mtls)
- .endpoint("foo", "c0")
- .build();
- tester.provisionLoadBalancers(1, context.instanceId(), testZonePublic, stagingZonePublic);
- tester.provisionLoadBalancers(1, context.instanceId(), zone2);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.test, Environment.staging, Environment.prod).deploy();
- assertEquals(List.of("a6414896.cafed00d.aws-eu-west-1.w.vespa-app.cloud",
- "a9c8c045.cafed00d.g.vespa-app.cloud",
- "cbff1506.cafed00d.z.vespa-app.cloud",
- "ebd395b6.cafed00d.z.vespa-app.cloud",
- "fcf1bd63.cafed00d.aws-us-east-1.w.vespa-app.cloud"),
- tester.recordNames());
- }
-
- @Test
- public void combined_endpoint_config_with_multiple_instances() {
- var tester = new RoutingPoliciesTester(SystemName.Public).setEndpointConfig(EndpointConfig.combined);
- var context0 = tester.newDeploymentContext("tenant1", "app1", "default");
- var context1 = tester.newDeploymentContext("tenant1", "app1", "beta");
-
- // Deploy application
- int clustersPerZone = 1;
- var zone1 = ZoneId.from("prod", "aws-us-east-1c");
- ApplicationPackage applicationPackage = applicationPackageBuilder().instances("default,beta")
- .region(zone1.region())
- .container("c0", AuthMethod.mtls)
- .applicationEndpoint("a0", "c0", Map.of(zone1.region().value(),
- Map.of(context0.instanceId().instance(), 1,
- context1.instanceId().instance(), 1)))
- .build();
- tester.provisionLoadBalancers(clustersPerZone, context0.instanceId(), zone1);
- tester.provisionLoadBalancers(clustersPerZone, context1.instanceId(), zone1);
- context0.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- assertEquals(List.of("a0.app1.tenant1.a.vespa-app.cloud",
- "c0.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud",
- "c0.beta.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud",
- "cbff1506.f5549014.z.vespa-app.cloud",
- "e144a11b.f5549014.a.vespa-app.cloud",
- "ee82b867.f5549014.z.vespa-app.cloud"),
- tester.recordNames());
- tester.assertTargets(context0.application().id(), EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
- Map.of(context0.deploymentIdIn(zone1), 1, context1.deploymentIdIn(zone1), 1));
-
- // Remove one instance from application endpoint
- applicationPackage = applicationPackageBuilder().instances("default,beta")
- .region(zone1.region())
- .container("c0", AuthMethod.mtls)
- .applicationEndpoint("a0", "c0", Map.of(zone1.region().value(),
- Map.of(context1.instanceId().instance(), 1)))
- .build();
- context0.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- assertEquals(List.of("a0.app1.tenant1.a.vespa-app.cloud",
- "c0.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud",
- "c0.beta.app1.tenant1.aws-us-east-1c.z.vespa-app.cloud",
- "cbff1506.f5549014.z.vespa-app.cloud",
- "e144a11b.f5549014.a.vespa-app.cloud",
- "ee82b867.f5549014.z.vespa-app.cloud"),
- tester.recordNames());
- tester.assertTargets(context0.application().id(), EndpointId.of("a0"), ClusterSpec.Id.from("c0"), 0,
- Map.of(context1.deploymentIdIn(zone1), 1));
-
- // Removing application removes all records
- context0.submit(ApplicationPackageBuilder.fromDeploymentXml("<deployment version='1.0'/>",
- ValidationId.deploymentRemoval,
- ValidationId.globalEndpointChange));
- context0.flushDnsUpdates();
- assertEquals(List.of(), tester.recordNames());
- }
-
- @Test
- public void migrate_legacy_to_combined_endpoint_config_with_global_endpoint() {
- var tester = new RoutingPoliciesTester(SystemName.Public).setEndpointConfig(EndpointConfig.legacy);
- var context = tester.newDeploymentContext("tenant1", "app1", "default");
-
- // Deploy application
- int clustersPerZone = 2;
- var zone1 = ZoneId.from("prod", "aws-us-east-1c");
- var zone2 = ZoneId.from("prod", "aws-eu-west-1a");
- ApplicationPackage applicationPackage = applicationPackageBuilder().region(zone1.region())
- .region(zone2.region())
- .container("c0", AuthMethod.mtls)
- .endpoint("foo", "c0")
- .build();
- tester.provisionLoadBalancers(clustersPerZone, context.instanceId(), zone1, zone2);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- tester.assertTargets(context.instanceId(), EndpointId.of("foo"), 0, zone1, zone2);
-
- // Switch to combined
- tester.setEndpointConfig(EndpointConfig.combined);
- context.submit(applicationPackage).deferLoadBalancerProvisioningIn(Environment.prod).deploy();
- tester.assertTargets(context.instance().id(), EndpointId.of("foo"), ClusterSpec.Id.from("c0"),
- 0, Map.of(zone1, 1L, zone2, 1L), true);
- }
-
- private void addCertificateToPool(String id, UnassignedCertificate.State state, RoutingPoliciesTester tester) {
- EndpointCertificate cert = new EndpointCertificate("testKey", "testCert", 1, 0,
- "request-id",
- Optional.of("leaf-request-uuid"),
- List.of("*." + id + ".z.vespa-app.cloud",
- "*." + id + ".g.vespa-app.cloud",
- "*." + id + ".a.vespa-app.cloud"),
- "",
- Optional.empty(),
- Optional.empty(),
- Optional.of(id));
- UnassignedCertificate pooledCert = new UnassignedCertificate(cert, state);
- tester.controllerTester().controller().curator().writeUnassignedCertificate(pooledCert);
- }
-
- /** Returns an application package builder that satisfies requirements for a directly routed endpoint */
- private static ApplicationPackageBuilder applicationPackageBuilder() {
- return new ApplicationPackageBuilder().athenzIdentity(AthenzDomain.from("domain"),
- AthenzService.from("service"));
- }
-
- private static List<LoadBalancer> createLoadBalancers(ZoneId zone, ApplicationId application, boolean shared, int count) {
- List<LoadBalancer> loadBalancers = new ArrayList<>();
- for (int i = 0; i < count; i++) {
- Optional<DomainName> lbHostname;
- Optional<String> ipAddress;
- if (zone.region().value().startsWith("gcp-")) {
- lbHostname = Optional.empty();
- ipAddress = Optional.of("10.0.0." + i);
- } else {
- String hostname = shared ? "shared-lb--" + zone.value() : "lb-" + i + "--" + application.toFullString() + "--" + zone.value();
- lbHostname = Optional.of(DomainName.of(hostname));
- ipAddress = Optional.empty();
- }
- loadBalancers.add(
- new LoadBalancer("LB-" + i + "-Z-" + zone.value(),
- application,
- ClusterSpec.Id.from("c" + i),
- lbHostname,
- ipAddress,
- LoadBalancer.State.active,
- Optional.of("dns-zone-1").filter(__ -> lbHostname.isPresent()),
- Optional.empty(),
- Optional.empty(),
- true));
- }
- return loadBalancers;
- }
-
- private static List<ZoneApi> publicZones() {
- return List.of(ZoneApiMock.newBuilder()
- .with(ZoneId.from(Environment.prod, RegionName.from("aws-us-east-1c")))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("us-east-1")
- .build(),
- ZoneApiMock.newBuilder()
- .with(ZoneId.from(Environment.prod, RegionName.from("aws-eu-west-1a")))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("eu-west-1")
- .build(),
- ZoneApiMock.newBuilder()
- .with(ZoneId.from(Environment.prod, RegionName.from("aws-us-east-1a")))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("us-east-1")
- .build(),
- ZoneApiMock.newBuilder()
- .with(ZoneId.from(Environment.prod, RegionName.from("gcp-us-south1-b")))
- .with(CloudName.GCP)
- .withCloudNativeRegionName("us-south1")
- .build(),
- ZoneApiMock.newBuilder()
- .with(ZoneId.from(Environment.staging, RegionName.from("aws-us-east-3c")))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("us-east-3")
- .build(),
- ZoneApiMock.newBuilder()
- .with(ZoneId.from(Environment.test, RegionName.from("aws-us-east-2c")))
- .with(CloudName.AWS)
- .withCloudNativeRegionName("us-east-2")
- .build(),
- ZoneApiMock.newBuilder()
- .with(ZoneId.from(Environment.staging, RegionName.from("gcp-us-east-99")))
- .with(CloudName.GCP)
- .withCloudNativeRegionName("us-east-99")
- .build(),
- ZoneApiMock.newBuilder()
- .with(ZoneId.from(Environment.test, RegionName.from("gcp-us-east-99")))
- .with(CloudName.GCP)
- .withCloudNativeRegionName("us-east-99")
- .build());
- }
-
- private static class RoutingPoliciesTester {
-
- private final DeploymentTester tester;
-
- public RoutingPoliciesTester() {
- this(SystemName.main);
- }
-
- public RoutingPoliciesTester(SystemName system) {
- this(new DeploymentTester(system.isPublic() ? new ControllerTester(new RotationsConfig.Builder().build(), system)
- : new ControllerTester(system)),
- true);
- }
-
- public RoutingPoliciesTester(DeploymentTester tester, boolean exclusiveRouting) {
- this.tester = tester;
- List<ZoneApi> zones;
- if (tester.controller().system().isPublic()) {
- zones = publicZones();
- } else {
- zones = new ArrayList<>(tester.controllerTester().zoneRegistry().zones().all().zones());
- zones.addAll(List.of(zoneApi1, zoneApi2, zoneApi3, zoneApi4, zoneApi5, zoneApi6));
- }
- tester.controllerTester().zoneRegistry().setZones(zones);
- tester.configServer().bootstrap(toZoneIds(zones), SystemApplication.notController());
- tester.controllerTester().setRoutingMethod(toZoneIds(zones), exclusiveRouting ? RoutingMethod.exclusive : RoutingMethod.sharedLayer4);
- }
-
- public Map<DeploymentId, Set<ContainerEndpoint>> containerEndpoints(Environment environment) {
- return tester.controllerTester().configServer().containerEndpoints().entrySet().stream()
- .filter(kv -> kv.getKey().zoneId().environment() == environment)
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
- }
-
- public RoutingPoliciesTester setEndpointConfig(EndpointConfig config) {
- tester.controllerTester().flagSource().withStringFlag(Flags.ENDPOINT_CONFIG.id(), config.name());
- return this;
- }
-
- public RoutingPolicies routingPolicies() {
- return tester.controllerTester().controller().routing().policies();
- }
-
- public DeploymentContext newDeploymentContext(String tenant, String application, String instance) {
- return tester.newDeploymentContext(tenant, application, instance);
- }
-
- public DeploymentContext newDeploymentContext(ApplicationId instance) {
- return tester.newDeploymentContext(instance);
- }
-
- public ControllerTester controllerTester() {
- return tester.controllerTester();
- }
-
- private List<ZoneId> toZoneIds(List<ZoneApi> zoneApis) {
- return zoneApis.stream().map(ZoneApi::getId).toList();
- }
-
- private void provisionLoadBalancers(int clustersPerZone, ApplicationId application, boolean shared, ZoneId... zones) {
- for (ZoneId zone : zones) {
- tester.configServer().removeLoadBalancers(application, zone);
- tester.configServer().putLoadBalancers(zone, createLoadBalancers(zone, application, shared, clustersPerZone));
- }
- }
-
- private void provisionLoadBalancers(int clustersPerZone, ApplicationId application, ZoneId... zones) {
- provisionLoadBalancers(clustersPerZone, application, false, zones);
- }
-
- private RoutingPolicyList policiesOf(ApplicationId instance) {
- return tester.controller().routing().policies().read(instance);
- }
-
- private List<String> recordNames() {
- return tester.controllerTester().nameService().records().stream()
- .map(Record::name)
- .map(RecordName::asString)
- .distinct()
- .sorted()
- .toList();
- }
-
- private Set<String> aliasDataOf(String name) {
- return tester.controllerTester().nameService().findRecords(Record.Type.ALIAS, RecordName.from(name)).stream()
- .map(Record::data)
- .map(RecordData::asString)
- .collect(Collectors.toSet());
- }
-
- private List<String> recordDataOf(Record.Type type, String name) {
- return tester.controllerTester().nameService().findRecords(type, RecordName.from(name)).stream()
- .map(Record::data)
- .map(RecordData::asString)
- .toList();
- }
-
- private void assertTargets(TenantAndApplicationId application, EndpointId endpointId, ClusterSpec.Id cluster,
- int loadBalancerId, Map<DeploymentId, Integer> deploymentWeights) {
- assertTargets(application, endpointId, cluster, loadBalancerId, deploymentWeights, false);
- }
-
- /** Assert that an application endpoint points to given targets and weights */
- private void assertTargets(TenantAndApplicationId application, EndpointId endpointId, ClusterSpec.Id cluster,
- int loadBalancerId, Map<DeploymentId, Integer> deploymentWeights, boolean generated) {
- Map<String, List<DeploymentId>> deploymentsByDnsName = new HashMap<>();
- for (var deployment : deploymentWeights.keySet()) {
- EndpointList applicationEndpoints = tester.controller().routing().readDeclaredEndpointsOf(tester.controller().applications().requireApplication(application))
- .named(endpointId, Endpoint.Scope.application)
- .targets(deployment)
- .cluster(cluster);
- if (generated) {
- applicationEndpoints = applicationEndpoints.generated();
- } else {
- applicationEndpoints = applicationEndpoints.not().generated();
- }
- assertEquals(1,
- applicationEndpoints.size(),
- "Expected a single endpoint with ID '" + endpointId + "'");
- String dnsName = applicationEndpoints.asList().get(0).dnsName();
- deploymentsByDnsName.computeIfAbsent(dnsName, (k) -> new ArrayList<>())
- .add(deployment);
- }
- assertFalse(deploymentsByDnsName.isEmpty(), "Found " + endpointId + " for " + application);
- deploymentsByDnsName.forEach((dnsName, deployments) -> {
- Set<String> weightedTargets = deployments.stream()
- .map(d -> "weighted/lb-" + loadBalancerId + "--" +
- d.applicationId().toFullString() + "--" + d.zoneId().value() +
- "/dns-zone-1/" + d.zoneId().value() + "/" + deploymentWeights.get(d))
- .collect(Collectors.toSet());
- assertEquals(weightedTargets, aliasDataOf(dnsName), dnsName + " has expected targets");
- });
- }
-
- private void assertTargets(ApplicationId instance, EndpointId endpointId, ClusterSpec.Id cluster,
- int loadBalancerId, Map<ZoneId, Long> zoneWeights) {
- assertTargets(instance, endpointId, cluster, loadBalancerId, zoneWeights, false);
- }
-
- /** Assert that a global endpoint points to given zones and weights */
- private void assertTargets(ApplicationId instance, EndpointId endpointId, ClusterSpec.Id cluster,
- int loadBalancerId, Map<ZoneId, Long> zoneWeights, boolean generated) {
- Set<String> latencyTargets = new HashSet<>();
- Map<String, List<ZoneId>> zonesByRegionEndpoint = new HashMap<>();
- for (var zone : zoneWeights.keySet()) {
- DeploymentId deployment = new DeploymentId(instance, zone);
- EndpointList regionEndpoints = tester.controller().routing().readEndpointsOf(deployment)
- .cluster(cluster)
- .scope(Endpoint.Scope.weighted);
- if (generated) {
- regionEndpoints = regionEndpoints.generated();
- } else {
- regionEndpoints = regionEndpoints.not().generated();
- }
- Endpoint regionEndpoint = regionEndpoints.first().orElseThrow(() -> new IllegalArgumentException("No" + (generated ? " generated" : "") + " region endpoint found for " + cluster + " in " + deployment));
- zonesByRegionEndpoint.computeIfAbsent(regionEndpoint.dnsName(), (k) -> new ArrayList<>())
- .add(zone);
- }
- zonesByRegionEndpoint.forEach((regionEndpoint, zonesInRegion) -> {
- Set<String> weightedTargets = zonesInRegion.stream()
- .map(z -> "weighted/lb-" + loadBalancerId + "--" +
- instance.toFullString() + "--" + z.value() +
- "/dns-zone-1/" + z.value() + "/" + zoneWeights.get(z))
- .collect(Collectors.toSet());
- assertEquals(weightedTargets,
- aliasDataOf(regionEndpoint),
- "Region endpoint " + regionEndpoint + " points to load balancer");
- ZoneId zone = zonesInRegion.get(0);
- String latencyTarget = "latency/" + regionEndpoint + "/dns-zone-1/" + zone.value();
- latencyTargets.add(latencyTarget);
- });
- List<DeploymentId> deployments = zoneWeights.keySet().stream().map(z -> new DeploymentId(instance, z)).toList();
- EndpointList global = tester.controller().routing().readDeclaredEndpointsOf(instance)
- .named(endpointId, Endpoint.Scope.global)
- .targets(deployments);
- if (generated) {
- global = global.generated();
- } else {
- global = global.not().generated();
- }
- String globalEndpoint = global.first()
- .map(Endpoint::dnsName)
- .orElse("<none>");
- assertEquals(latencyTargets, Set.copyOf(aliasDataOf(globalEndpoint)), "Global endpoint " + globalEndpoint + " points to expected latency targets");
-
- }
-
- /** Assert that a global endpoint points to given zones */
- private void assertTargets(ApplicationId application, EndpointId endpointId, int loadBalancerId, ZoneId... zones) {
- Map<ZoneId, Long> zoneWeights = new LinkedHashMap<>();
- for (var zone : zones) {
- zoneWeights.put(zone, 1L);
- }
- assertTargets(application, endpointId, ClusterSpec.Id.from("c" + loadBalancerId), loadBalancerId, zoneWeights);
- }
-
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationRepositoryTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationRepositoryTest.java
deleted file mode 100644
index 43be5631727..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/routing/rotation/RotationRepositoryTest.java
+++ /dev/null
@@ -1,205 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.routing.rotation;
-
-import com.yahoo.config.provision.RegionName;
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.zone.RoutingMethod;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.application.AssignedRotation;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock;
-import com.yahoo.vespa.hosted.rotation.config.RotationsConfig;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-import java.util.List;
-import java.util.Set;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * @author Oyvind Gronnesby
- * @author mpolden
- */
-public class RotationRepositoryTest {
-
- private static final RotationsConfig rotationsConfig = new RotationsConfig(
- new RotationsConfig.Builder()
- .rotations("foo-1", "foo-1.com")
- .rotations("foo-2", "foo-2.com")
- );
-
- private static final RotationsConfig rotationsConfigWhitespaces = new RotationsConfig(
- new RotationsConfig.Builder()
- .rotations("foo-1", "\n \t foo-1.com \n")
- .rotations("foo-2", "foo-2.com")
- );
-
- private static final ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "foo")
- .region("us-east-3")
- .region("us-west-1")
- .build();
-
- private final DeploymentTester tester = new DeploymentTester(new ControllerTester(rotationsConfig, SystemName.main));
- private final RotationRepository repository = tester.controller().routing().rotations();
- private final DeploymentContext application = tester.newDeploymentContext("tenant1", "app1", "default");
-
- @Test
- void assigns_and_reuses_rotation() {
- // Deploying assigns a rotation
- application.submit(applicationPackage).deploy();
- Rotation expected = new Rotation(new RotationId("foo-1"), "foo-1.com");
-
- assertEquals(List.of(expected.id()), rotationIds(application.instance().rotations()));
- assertEquals(URI.create("https://app1.tenant1.global.vespa.oath.cloud/"),
- tester.controller().routing().readDeclaredEndpointsOf(application.instanceId()).direct().first().get().url());
- try (RotationLock lock = repository.lock()) {
- List<AssignedRotation> rotations = repository.getOrAssignRotations(application.application().deploymentSpec(),
- application.instance(),
- lock);
- assertSingleRotation(expected, rotations, repository);
- assertEquals(Set.of(RegionName.from("us-west-1"), RegionName.from("us-east-3")),
- application.instance().rotations().get(0).regions());
- }
-
- // Submitting once more assigns same rotation
- application.submit(applicationPackage).deploy();
- assertEquals(List.of(expected.id()), rotationIds(application.instance().rotations()));
-
- // Adding region updates rotation
- var applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "foo")
- .region("us-east-3")
- .region("us-west-1")
- .region("us-central-1")
- .build();
- application.submit(applicationPackage).deploy();
- assertEquals(Set.of(RegionName.from("us-west-1"), RegionName.from("us-east-3"),
- RegionName.from("us-central-1")),
- application.instance().rotations().get(0).regions());
- }
-
- @Test
- void strips_whitespace_in_rotation_fqdn() {
- var tester = new DeploymentTester(new ControllerTester(rotationsConfigWhitespaces, SystemName.main));
- RotationRepository repository = tester.controller().routing().rotations();
- var application2 = tester.newDeploymentContext("tenant1", "app2", "default");
-
- application2.submit(applicationPackage);
-
- try (RotationLock lock = repository.lock()) {
- List<AssignedRotation> rotations = repository.getOrAssignRotations(application2.application().deploymentSpec(), application2.instance(), lock);
- Rotation assignedRotation = new Rotation(new RotationId("foo-1"), "foo-1.com");
- assertSingleRotation(assignedRotation, rotations, repository);
- }
- }
-
- @Test
- void out_of_rotations() {
- // Assigns 1 rotation
- application.submit(applicationPackage).deploy();
-
- // Assigns 1 more
- var application2 = tester.newDeploymentContext("tenant2", "app2", "default");
- application2.submit(applicationPackage).deploy();
-
- // We're now out of rotations and next deployment fails
- var application3 = tester.newDeploymentContext("tenant3", "app3", "default");
- application3.submit(applicationPackage)
- .runJobExpectingFailure(DeploymentContext.systemTest, "out of rotations");
- }
-
- @Test
- void no_rotation_assigned_for_application_without_service_id() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .region("us-east-3")
- .region("us-west-1")
- .build();
- application.submit(applicationPackage);
- assertTrue(application.instance().rotations().isEmpty());
- }
-
- @Test
- void prefixes_system_when_not_main() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .endpoint("default", "foo")
- .region("cd-us-east-1")
- .region("cd-us-west-1")
- .build();
- var zones = List.of(
- ZoneApiMock.fromId("test.cd-us-west-1"),
- ZoneApiMock.fromId("staging.cd-us-west-1"),
- ZoneApiMock.fromId("prod.cd-us-east-1"),
- ZoneApiMock.fromId("prod.cd-us-west-1"));
- DeploymentTester tester = new DeploymentTester(new ControllerTester(rotationsConfig, SystemName.cd));
- tester.controllerTester().zoneRegistry()
- .setZones(zones)
- .setRoutingMethod(zones, RoutingMethod.sharedLayer4);
- tester.configServer().bootstrap(tester.controllerTester().zoneRegistry().zones().all().ids(), SystemApplication.notController());
- var application2 = tester.newDeploymentContext("tenant2", "app2", "default");
- application2.submit(applicationPackage).deploy();
- assertEquals(List.of(new RotationId("foo-1")), rotationIds(application2.instance().rotations()));
- assertEquals("https://cd.app2.tenant2.global.cd.vespa.oath.cloud/",
- tester.controller().routing().readDeclaredEndpointsOf(application2.instanceId()).first().get().url().toString());
- }
-
- @Test
- void multiple_instances_with_similar_global_service_id() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .instances("instance1,instance2")
- .region("us-central-1")
- .parallel("us-west-1", "us-east-3")
- .endpoint("default", "global")
- .build();
- var instance1 = tester.newDeploymentContext("tenant1", "application1", "instance1")
- .submit(applicationPackage)
- .deploy();
- var instance2 = tester.newDeploymentContext("tenant1", "application1", "instance2");
- assertEquals(List.of(new RotationId("foo-1")), rotationIds(instance1.instance().rotations()));
- assertEquals(List.of(new RotationId("foo-2")), rotationIds(instance2.instance().rotations()));
- assertEquals(URI.create("https://instance1.application1.tenant1.global.vespa.oath.cloud/"),
- tester.controller().routing().readDeclaredEndpointsOf(instance1.instanceId()).direct().first().get().url());
- assertEquals(URI.create("https://instance2.application1.tenant1.global.vespa.oath.cloud/"),
- tester.controller().routing().readDeclaredEndpointsOf(instance2.instanceId()).direct().first().get().url());
- }
-
- @Test
- void multiple_instances_with_similar_endpoints() {
- ApplicationPackage applicationPackage = new ApplicationPackageBuilder()
- .instances("instance1,instance2")
- .region("us-central-1")
- .parallel("us-west-1", "us-east-3")
- .endpoint("default", "foo", "us-central-1", "us-west-1")
- .build();
- var instance1 = tester.newDeploymentContext("tenant1", "application1", "instance1")
- .submit(applicationPackage)
- .deploy();
- var instance2 = tester.newDeploymentContext("tenant1", "application1", "instance2");
-
- assertEquals(List.of(new RotationId("foo-1")), rotationIds(instance1.instance().rotations()));
- assertEquals(List.of(new RotationId("foo-2")), rotationIds(instance2.instance().rotations()));
-
- assertEquals(URI.create("https://instance1.application1.tenant1.global.vespa.oath.cloud/"),
- tester.controller().routing().readDeclaredEndpointsOf(instance1.instanceId()).direct().first().get().url());
- assertEquals(URI.create("https://instance2.application1.tenant1.global.vespa.oath.cloud/"),
- tester.controller().routing().readDeclaredEndpointsOf(instance2.instanceId()).direct().first().get().url());
- }
-
- private void assertSingleRotation(Rotation expected, List<AssignedRotation> assignedRotations, RotationRepository repository) {
- assertEquals(1, assignedRotations.size());
- RotationId rotationId = assignedRotations.get(0).rotationId();
- Rotation rotation = repository.requireRotation(rotationId);
- assertEquals(expected, rotation);
- }
-
- private static List<RotationId> rotationIds(List<AssignedRotation> assignedRotations) {
- return assignedRotations.stream().map(AssignedRotation::rotationId).toList();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManagerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManagerTest.java
deleted file mode 100644
index 16485019355..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/security/CloudUserSessionManagerTest.java
+++ /dev/null
@@ -1,64 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.security;
-
-import com.yahoo.config.provision.SystemName;
-import com.yahoo.config.provision.TenantName;
-import com.yahoo.vespa.flags.InMemoryFlagSource;
-import com.yahoo.vespa.flags.PermanentFlags;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.LockedTenant;
-import com.yahoo.vespa.hosted.controller.api.role.Role;
-import com.yahoo.vespa.hosted.controller.api.role.SecurityContext;
-import com.yahoo.vespa.hosted.controller.api.role.SimplePrincipal;
-import com.yahoo.vespa.hosted.controller.api.role.TenantRole;
-import org.junit.jupiter.api.Test;
-
-import java.time.Instant;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author freva
- */
-class CloudUserSessionManagerTest {
-
- private final ControllerTester tester = new ControllerTester(SystemName.Public);
- private final CloudUserSessionManager userSessionManager = new CloudUserSessionManager(tester.controller());
-
- @Test
- void test() {
- createTenant("tenant1", null);
- createTenant("tenant2", 1234);
- createTenant("tenant3", 1543);
- createTenant("tenant4", 2313);
-
- assertShouldExpire(false, 123);
- assertShouldExpire(false, 123, "tenant1");
- assertShouldExpire(true, 123, "tenant2");
- assertShouldExpire(false, 2123, "tenant2");
- assertShouldExpire(true, 123, "tenant1", "tenant2");
-
- ((InMemoryFlagSource) tester.controller().flagSource()).withLongFlag(PermanentFlags.INVALIDATE_CONSOLE_SESSIONS.id(), 150);
- assertShouldExpire(true, 123);
- assertShouldExpire(true, 123, "tenant1");
- }
-
- private void assertShouldExpire(boolean expected, long issuedAtSeconds, String... tenantNames) {
- Set<Role> roles = Stream.of(tenantNames).map(name -> TenantRole.developer(TenantName.from(name))).collect(Collectors.toSet());
- SecurityContext context = new SecurityContext(new SimplePrincipal("dev"), roles, Instant.ofEpochSecond(issuedAtSeconds));
- assertEquals(expected, userSessionManager.shouldExpireSessionFor(context));
- }
-
- private void createTenant(String tenantName, Integer invalidateAfterSeconds) {
- tester.createTenant(tenantName);
- Optional.ofNullable(invalidateAfterSeconds)
- .map(Instant::ofEpochSecond)
- .ifPresent(instant ->
- tester.controller().tenants().lockOrThrow(TenantName.from(tenantName), LockedTenant.Cloud.class, tenant ->
- tester.controller().tenants().store(tenant.withInvalidateUserSessionsBefore(instant))));
- }
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/Keys.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/Keys.java
deleted file mode 100644
index e3e76920bca..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/Keys.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tls;
-
-import com.yahoo.security.KeyAlgorithm;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.security.SignatureAlgorithm;
-import com.yahoo.security.X509CertificateBuilder;
-
-import javax.security.auth.x500.X500Principal;
-import java.math.BigInteger;
-import java.security.KeyPair;
-import java.security.cert.X509Certificate;
-import java.time.Duration;
-import java.time.Instant;
-
-/**
- * @author mpolden
- */
-public class Keys {
-
- public static final KeyPair keyPair = KeyUtils.generateKeypair(KeyAlgorithm.EC, 256);
- public static final X509Certificate certificate = createCertificate(keyPair);
-
- private static X509Certificate createCertificate(KeyPair keyPair) {
- Instant now = Instant.now();
- return X509CertificateBuilder.fromKeypair(keyPair, new X500Principal("CN=localhost"), now,
- now.plus(Duration.ofDays(1)),
- SignatureAlgorithm.SHA512_WITH_ECDSA,
- BigInteger.valueOf(1))
- .build();
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecretStoreMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecretStoreMock.java
deleted file mode 100644
index 2498668e28a..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecretStoreMock.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tls;
-
-import com.yahoo.component.annotation.Inject;
-import com.yahoo.security.KeyUtils;
-import com.yahoo.security.X509CertificateUtils;
-import com.yahoo.vespa.hosted.controller.tls.config.TlsConfig;
-
-/**
- * A secret store mock that's pre-populated with a certificate and key.
- *
- * @author mpolden
- */
-@SuppressWarnings("unused") // Injected
-public class SecretStoreMock extends com.yahoo.vespa.hosted.controller.integration.SecretStoreMock {
-
- @Inject
- public SecretStoreMock(TlsConfig config) {
- addKeyPair(config);
- }
-
- private void addKeyPair(TlsConfig config) {
- setSecret(config.privateKeySecret(), KeyUtils.toPem(Keys.keyPair.getPrivate()));
- setSecret(config.certificateSecret(), X509CertificateUtils.toPem(Keys.certificate));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecureContainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecureContainerTest.java
deleted file mode 100644
index 76f3b229028..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/tls/SecureContainerTest.java
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.tls;
-
-import com.yahoo.application.Networking;
-import com.yahoo.application.container.JDisc;
-import com.yahoo.application.container.handler.Request;
-import com.yahoo.application.container.handler.Response;
-import com.yahoo.component.ComponentId;
-import com.yahoo.security.KeyStoreBuilder;
-import com.yahoo.security.KeyStoreType;
-import com.yahoo.security.KeyStoreUtils;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Path;
-import java.security.KeyStore;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-
-/**
- * @author mpolden
- */
-public class SecureContainerTest {
-
- private JDisc container;
-
- @TempDir
- public File folder;
-
- @BeforeEach
- public void startContainer() {
- container = JDisc.fromServicesXml(servicesXml(writeKeyStore()), Networking.enable);
- }
-
- @AfterEach
- public void stopContainer() {
- container.close();
- }
-
- @Test
- void test_https_request() {
- assertNotNull(sslContextFactoryProvider(), "SslContextFactoryProvider is created");
- assertResponse(Request.Method.GET, "/", 200);
- }
-
- private void assertResponse(Request.Method method, String path, int expectedStatusCode) {
- Response response = container.handleRequest(new Request("https://localhost:9999" + path, new byte[0], method));
- assertEquals(expectedStatusCode, response.getStatus(), "Status code");
- }
-
- private ControllerSslContextFactoryProvider sslContextFactoryProvider() {
- return (ControllerSslContextFactoryProvider) container.components().getComponent(ComponentId.fromString("ssl-provider@default"));
- }
-
- private String servicesXml(Path trustStore) {
- return "<container version='1.0'>\n" +
- " <config name=\"container.handler.threadpool\">\n" +
- " <maxthreads>10</maxthreads>\n" +
- " </config> \n" +
- " <config name='vespa.hosted.controller.tls.config.tls'>\n" +
- " <caTrustStore>" + trustStore.toString() + "</caTrustStore>\n" +
- " <certificateSecret>controller.cert</certificateSecret>\n" +
- " <privateKeySecret>controller.key</privateKeySecret>\n" +
- " </config>\n" +
- " <component id='com.yahoo.vespa.hosted.controller.tls.SecretStoreMock'/>\n" +
- " <http>\n" +
- " <server id='default' port='9999'>\n" +
- " <ssl-provider class='com.yahoo.vespa.hosted.controller.tls.ControllerSslContextFactoryProvider' bundle='controller-server'/>\n" +
- " </server>\n" +
- " </http>\n" +
- "</container>";
- }
-
- private Path writeKeyStore() {
- KeyStore keyStore = KeyStoreBuilder.withType(KeyStoreType.JKS)
- .withKeyEntry(getClass().getSimpleName(),
- Keys.keyPair.getPrivate(), new char[0], Keys.certificate)
- .build();
- try {
- Path path = File.createTempFile("junit", null, folder).toPath();
- KeyStoreUtils.writeKeyStoreToFile(keyStore, path);
- return path;
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/MavenRepositoryClientTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/MavenRepositoryClientTest.java
deleted file mode 100644
index 1d9a98df0df..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/MavenRepositoryClientTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.vespa.hosted.controller.api.integration.maven.ArtifactId;
-import org.junit.jupiter.api.Test;
-
-import java.net.URI;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * @author jonmv
- */
-public class MavenRepositoryClientTest {
-
- @Test
- void testUri() {
- assertEquals(URI.create("https://domain:123/base/group/id/artifact-id/maven-metadata.xml"),
- MavenRepositoryClient.withArtifactPath(URI.create("https://domain:123/base/"),
- new ArtifactId("group.id", "artifact-id")));
- }
-
-}
diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java
deleted file mode 100644
index 4c46dbf6142..00000000000
--- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/versions/VersionStatusTest.java
+++ /dev/null
@@ -1,837 +0,0 @@
-// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.vespa.hosted.controller.versions;
-
-import com.yahoo.component.Version;
-import com.yahoo.config.provision.ApplicationId;
-import com.yahoo.config.provision.HostName;
-import com.yahoo.config.provision.zone.ZoneApi;
-import com.yahoo.vespa.hosted.controller.Controller;
-import com.yahoo.vespa.hosted.controller.ControllerTester;
-import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.Node;
-import com.yahoo.vespa.hosted.controller.api.integration.configserver.NodeFilter;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobId;
-import com.yahoo.vespa.hosted.controller.api.integration.deployment.JobType;
-import com.yahoo.vespa.hosted.controller.application.Change;
-import com.yahoo.vespa.hosted.controller.application.SystemApplication;
-import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage;
-import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentContext;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTester;
-import com.yahoo.vespa.hosted.controller.deployment.DeploymentTrigger.ChangesToCancel;
-import com.yahoo.vespa.hosted.controller.deployment.Run;
-import com.yahoo.vespa.hosted.controller.persistence.CuratorDb;
-import com.yahoo.vespa.hosted.controller.persistence.MockCuratorDb;
-import com.yahoo.vespa.hosted.controller.versions.VespaVersion.Confidence;
-import org.junit.jupiter.api.Test;
-
-import java.time.Duration;
-import java.time.Instant;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsEast3;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.productionUsWest1;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.stagingTest;
-import static com.yahoo.vespa.hosted.controller.deployment.DeploymentContext.systemTest;
-import static java.util.stream.Collectors.toSet;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertSame;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-/**
- * Test computing of version status
- *
- * @author bratseth
- */
-public class VersionStatusTest {
-
- @Test
- public void testEmptyVersionStatus() {
- VersionStatus status = VersionStatus.empty();
- assertFalse(status.systemVersion().isPresent());
- assertTrue(status.versions().isEmpty());
- }
-
- @Test
- public void testSystemVersionIsControllerVersionIfConfigServersAreNewer() {
- ControllerTester tester = new ControllerTester();
- Version controllerVersion = tester.controller().readVersionStatus().controllerVersion().get().versionNumber();
- Version largerThanCurrent = new Version(controllerVersion.getMajor() + 1);
- tester.upgradeSystemApplications(largerThanCurrent);
- VersionStatus versionStatus = VersionStatus.compute(tester.controller());
- assertEquals(controllerVersion, versionStatus.systemVersion().get().versionNumber());
- }
-
- @Test
- public void testSystemVersionIsVersionOfOldestConfigServer() {
- ControllerTester tester = new ControllerTester();
- Version version0 = Version.fromString("6.1");
- Version version1 = Version.fromString("6.5");
- // Upgrade some config servers
- for (ZoneApi zone : tester.zoneRegistry().zones().all().zones()) {
- for (Node node : tester.configServer().nodeRepository().list(zone.getId(), NodeFilter.all().applications(SystemApplication.configServer.id()))) {
- Node upgradedNode = Node.builder(node).currentVersion(version1).build();
- tester.configServer().nodeRepository().putNodes(zone.getId(), upgradedNode);
- break;
- }
- }
- VersionStatus versionStatus = VersionStatus.compute(tester.controller());
- assertEquals(version0, versionStatus.systemVersion().get().versionNumber());
- }
-
- @Test
- public void testControllerVersion() {
- HostName controller1 = HostName.of("controller-1");
- HostName controller2 = HostName.of("controller-2");
- HostName controller3 = HostName.of("controller-3");
- MockCuratorDb db = new MockCuratorDb(Stream.of(controller1, controller2, controller3)
- .map(hostName -> hostName.value() + ":2222")
- .collect(Collectors.joining(",")));
- ControllerTester tester = new ControllerTester(db);
-
- // Upgrade in progress
- writeControllerVersion(controller1, Version.fromString("6.2"), db);
- writeControllerVersion(controller2, Version.fromString("6.1"), db);
- writeControllerVersion(controller3, Version.fromString("6.2"), db);
- VersionStatus versionStatus = VersionStatus.compute(tester.controller());
- assertTrue(versionStatus.controllerVersion().isEmpty(), "Controller version is unknown during upgrade");
-
- // Last controller upgrades
- writeControllerVersion(controller2, Version.fromString("6.2"), db);
- versionStatus = VersionStatus.compute(tester.controller());
- assertEquals(Version.fromString("6.2"), versionStatus.controllerVersion().get().versionNumber());
- }
-
- @Test
- public void testSystemVersionNeverShrinks() {
- ControllerTester tester = new ControllerTester();
- Version version0 = Version.fromString("6.2");
- tester.upgradeSystem(version0);
- assertEquals(version0, tester.controller().readSystemVersion());
-
- // Downgrade one config server in each zone
- Version ancientVersion = Version.fromString("5.1");
- for (ZoneApi zone : tester.controller().zoneRegistry().zones().all().zones()) {
- for (Node node : tester.configServer().nodeRepository().list(zone.getId(), NodeFilter.all().applications(SystemApplication.configServer.id()))) {
- Node downgradedNode = Node.builder(node).currentVersion(ancientVersion).build();
- tester.configServer().nodeRepository().putNodes(zone.getId(), downgradedNode);
- break;
- }
- }
-
- tester.computeVersionStatus();
- assertEquals(version0, tester.controller().readSystemVersion());
- }
-
- @Test
- public void testVersionStatusAfterApplicationUpdates() {
- DeploymentTester tester = new DeploymentTester();
- ApplicationPackage applicationPackage = applicationPackage("default");
-
- Version version0 = new Version("6.1");
- tester.controllerTester().upgradeSystem(version0);
- var context0 = tester.newDeploymentContext("tenant1", "app0", "default").runJob(JobType.dev("us-east-1"), applicationPackage);
-
- Version version1 = new Version("6.2");
- Version version2 = new Version("6.3");
- tester.controllerTester().upgradeSystem(version1);
-
- // Setup applications
- var context1 = tester.newDeploymentContext("tenant1", "app1", "default").submit(applicationPackage).deploy();
- var context2 = tester.newDeploymentContext("tenant1", "app2", "default").submit(applicationPackage).deploy();
- var context3 = tester.newDeploymentContext("tenant1", "app3", "default").submit(applicationPackage).deploy();
-
- // version2 is released
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- // - app1 is in production on version1, but then fails in system test on version2
- context1.timeOutConvergence(systemTest);
- // - app2 is partially in production on version1 and partially on version2
- context2.runJob(systemTest)
- .runJob(stagingTest)
- .runJob(productionUsWest1)
- .failDeployment(productionUsEast3);
- // - app3 is in production on version1, but then fails in staging test on version2
- context3.timeOutUpgrade(stagingTest);
-
- tester.triggerJobs();
- tester.controllerTester().computeVersionStatus();
- VersionStatus status = tester.controller().readVersionStatus();
- assertEquals(3, status.versions().size(), "The three versions above exist");
-
- tester.controller().applications().deactivate(context0.instanceId(), JobType.dev("us-east-1").zone());
- tester.controllerTester().computeVersionStatus();
- List<VespaVersion> versions = tester.controller().readVersionStatus().versions();
- assertEquals(2, versions.size(), "The two last versions above exist after dev deployment is gone");
-
- VespaVersion v1 = versions.get(0);
- assertEquals(version1, v1.versionNumber());
- var statistics = DeploymentStatistics.compute(List.of(version1, version2), tester.deploymentStatuses());
- var statistics1 = statistics.get(0);
- assertJobsRun("No runs are failing on version1.",
- Map.of(context1.instanceId(), List.of(),
- context2.instanceId(), List.of(),
- context3.instanceId(), List.of()),
- statistics1.failingUpgrades());
- assertJobsRun("All applications have at least one active production deployment on version 1.",
- Map.of(context1.instanceId(), List.of(productionUsWest1, productionUsEast3),
- context2.instanceId(), List.of(productionUsEast3),
- context3.instanceId(), List.of(productionUsWest1, productionUsEast3)),
- statistics1.productionSuccesses());
- assertEquals(
- List.of(),
- statistics1.runningUpgrade(), "No applications have active deployment jobs on version1.");
-
- VespaVersion v2 = versions.get(1);
- assertEquals(version2, v2.versionNumber());
- var statistics2 = statistics.get(1);
- assertJobsRun("All applications have failed on version2 in at least one zone.",
- Map.of(context1.instanceId(), List.of(systemTest),
- context2.instanceId(), List.of(productionUsEast3),
- context3.instanceId(), List.of(stagingTest)),
- statistics2.failingUpgrades());
- assertJobsRun("Only app2 has successfully deployed to production on version2.",
- Map.of(context1.instanceId(), List.of(),
- context2.instanceId(), List.of(productionUsWest1),
- context3.instanceId(), List.of()),
- statistics2.productionSuccesses());
- assertJobsRun("All applications are being retried on version2.",
- Map.of(context1.instanceId(), List.of(systemTest, stagingTest),
- context2.instanceId(), List.of(productionUsEast3),
- context3.instanceId(), List.of(systemTest, stagingTest)),
- statistics2.runningUpgrade());
- }
-
- private static void assertJobsRun(String assertion, Map<ApplicationId, List<JobType>> jobs, List<Run> runs) {
- assertEquals(jobs.entrySet().stream()
- .flatMap(entry -> entry.getValue().stream().map(type -> new JobId(entry.getKey(), type)))
- .collect(toSet()),
- runs.stream()
- .map(run -> run.id().job())
- .collect(toSet()),
- assertion);
- }
-
- @Test
- public void testVersionConfidence() {
- DeploymentTester tester = new DeploymentTester().atMondayMorning();
- Version version0 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version0);
- tester.upgrader().maintain();
-
- // Setup applications - all running on version0
- ApplicationPackage canaryPolicy = applicationPackage("canary", 7);
- var canary0 = tester.newDeploymentContext("tenant1", "canary0", "default")
- .submit(canaryPolicy)
- .deploy();
- var canary1 = tester.newDeploymentContext("tenant1", "canary1", "default")
- .submit(canaryPolicy)
- .deploy();
- var canary2 = tester.newDeploymentContext("tenant1", "canary2", "default")
- .submit(canaryPolicy)
- .deploy();
-
- ApplicationPackage defaultPolicy = applicationPackage("default");
- var default0 = tester.newDeploymentContext("tenant1", "default0", "default")
- .submit(applicationPackage("default", 7))
- .deploy();
- var default1 = tester.newDeploymentContext("tenant1", "default1", "default")
- .submit(defaultPolicy)
- .deploy();
- var default2 = tester.newDeploymentContext("tenant1", "default2", "default")
- .submit(defaultPolicy)
- .deploy();
- var default3 = tester.newDeploymentContext("tenant1", "default3", "default")
- .submit(defaultPolicy)
- .deploy();
- var default4 = tester.newDeploymentContext("tenant1", "default4", "default")
- .submit(defaultPolicy)
- .deploy();
- var default5 = tester.newDeploymentContext("tenant1", "default5", "default")
- .submit(defaultPolicy)
- .deploy();
- var default6 = tester.newDeploymentContext("tenant1", "default6", "default")
- .submit(defaultPolicy)
- .deploy();
- var default7 = tester.newDeploymentContext("tenant1", "default7", "default")
- .submit(defaultPolicy)
- .deploy();
- var default8 = tester.newDeploymentContext("tenant1", "default8", "default")
- .submit(defaultPolicy)
- .deploy();
- var default9 = tester.newDeploymentContext("tenant1", "default9", "default")
- .submit(defaultPolicy)
- .deploy();
-
- ApplicationPackage conservativePolicy = applicationPackage("conservative");
- var conservative0 = tester.newDeploymentContext("tenant1", "conservative0", "default")
- .submit(conservativePolicy)
- .deploy();
-
- var devApp = tester.newDeploymentContext("dev", "app", "on-version-1");
- // Applications that do not affect confidence calculation:
-
- // Application without deployment
- var ignored0 = tester.newDeploymentContext("tenant1", "ignored0", "default");
-
- assertEquals(Confidence.high, confidence(tester.controller(), version0), "All applications running on this version: High");
-
- // New version is released
- Version version1 = new Version("6.3");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- // Dev app deploys to the new versions, and canaries also upgrade, and fail.
- devApp.runJob(JobType.dev("us-east-1"), canaryApplicationPackage);
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.low, confidence(tester.controller(), version1), "Just the dev app: Low");
-
- canary0.deployPlatform(version1);
- canary1.runJob(systemTest)
- .runJob(stagingTest)
- .failDeployment(productionUsWest1);
-
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.broken, confidence(tester.controller(), version1), "One canary failed: Broken");
-
- // New version is released
- Version version2 = new Version("6.4");
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(Confidence.low, confidence(tester.controller(), version2), "Confidence defaults to low for version with no applications");
-
- // All canaries upgrade successfully
- canary0.deployPlatform(version2);
- canary1.deployPlatform(version2);
-
- assertEquals(Confidence.broken, confidence(tester.controller(), version1), "Confidence remains unchanged for version1: Broken");
- assertEquals(Confidence.low, confidence(tester.controller(), version2), "Nothing has failed but not all canaries have upgraded: Low");
-
- // Remaining canary upgrades to version2 which raises confidence to normal and more apps upgrade
- canary2.triggerJobs().jobAborted(systemTest).jobAborted(stagingTest);
- canary2.runJob(stagingTest);
- canary2.deployPlatform(version2);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(Confidence.normal, confidence(tester.controller(), version2), "Canaries have upgraded: Normal");
- default0.deployPlatform(version2);
- default1.deployPlatform(version2);
- default2.deployPlatform(version2);
- default3.deployPlatform(version2);
- default4.deployPlatform(version2);
- default5.deployPlatform(version2);
- default6.deployPlatform(version2);
- default7.deployPlatform(version2);
- tester.controllerTester().computeVersionStatus();
-
- // Remember confidence across restart
- tester.controllerTester().createNewController();
-
- assertEquals(Confidence.high, confidence(tester.controller(), version0), "Confidence remains unchanged for version0: High");
- assertEquals(Confidence.normal, confidence(tester.controller(), version2), "All canaries deployed + < 90% of defaults: Normal");
- assertTrue(tester.controller().readVersionStatus().versions().stream()
- .noneMatch(vespaVersion -> vespaVersion.versionNumber().equals(version1)),
- "Status for version without applications is removed");
-
- // Another default application upgrades, raising confidence to high
- default8.deployPlatform(version2);
- default9.deployPlatform(version2);
- tester.controllerTester().computeVersionStatus();
-
- assertEquals(Confidence.high, confidence(tester.controller(), version0), "Confidence remains unchanged for version0: High");
- assertEquals(VespaVersion.Confidence.high, confidence(tester.controller(), version2), "90% of defaults deployed successfully: High");
-
- // Canary failing a new revision does not affect confidence
- canary0.submit(canaryPolicy).failDeployment(systemTest);
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.high, confidence(tester.controller(), version0), "Confidence remains unchanged for version0: High");
- assertEquals(VespaVersion.Confidence.high, confidence(tester.controller(), version2), "90% of defaults deployed successfully: High");
- canary0.deploy();
-
- // A new version is released, all canaries upgrade successfully, but enough "default" apps fail to mark version
- // as broken
- Version version3 = new Version("6.5");
- tester.controllerTester().upgradeSystem(version3);
- tester.upgrader().maintain();
- tester.triggerJobs();
- canary0.deployPlatform(version3);
- canary1.deployPlatform(version3);
- canary2.deployPlatform(version3);
- tester.controllerTester().computeVersionStatus();
- tester.upgrader().maintain();
- tester.triggerJobs();
- default0.failDeployment(stagingTest);
- default1.failDeployment(stagingTest);
- default2.failDeployment(stagingTest);
- default3.failDeployment(stagingTest);
- default4.failDeployment(stagingTest);
- default5.failDeployment(stagingTest);
- tester.controllerTester().computeVersionStatus();
-
- assertEquals(Confidence.high, confidence(tester.controller(), version0), "Confidence remains unchanged for version0: High");
- assertEquals(Confidence.high, confidence(tester.controller(), version2), "Confidence remains unchanged for version2: High");
- assertEquals(VespaVersion.Confidence.broken, confidence(tester.controller(), version3), "60% of defaults failed: Broken");
-
- // Test version order
- List<VespaVersion> versions = tester.controller().readVersionStatus().versions();
- assertEquals(List.of("6.2", "6.4", "6.5"), versions.stream().map(version -> version.versionNumber().toString()).toList());
-
- // Check release status is correct (static data in MockMavenRepository, and upgradeSystem "releases" a version).
- assertTrue(versions.get(0).isReleased());
- assertFalse(versions.get(1).isReleased()); // test quirk: maven repo lost during controller recreation; useful to test status though
- assertFalse(versions.get(2).isReleased());
-
- tester.clock().advance(Duration.ofSeconds(10801));
- tester.controllerTester().computeVersionStatus();
- versions = tester.controller().readVersionStatus().versions();
- assertTrue(versions.get(2).isReleased());
-
- // A new major version is released and all canaries upgrade
- Version version4 = new Version("7.1");
- tester.controllerTester().upgradeSystem(version4);
- tester.upgrader().maintain();
- tester.triggerJobs();
- canary0.deployPlatform(version4);
- canary1.deployPlatform(version4);
- canary2.deployPlatform(version4);
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.normal, confidence(tester.controller(), version4));
-
- // The single application allowing this major upgrades and confidence becomes 'high'
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(Change.of(version4), default0.instance().change());
- default0.jobAborted(systemTest)
- .jobAborted(stagingTest)
- .deployPlatform(version4);
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.high, confidence(tester.controller(), version4));
- }
-
- @Test
- public void testConfidenceWithLingeringVersions() {
- DeploymentTester tester = new DeploymentTester().atMondayMorning();
- Version version0 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version0);
- tester.upgrader().maintain();
- var appPackage = applicationPackage("canary");
-
- var canary0 = tester.newDeploymentContext("tenant1", "canary0", "default")
- .submit(appPackage)
- .deploy();
-
- assertEquals(
- Confidence.high, confidence(tester.controller(), version0), "All applications running on this version: High");
-
- // New version is released
- Version version1 = new Version("6.3");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- tester.triggerJobs();
-
- // App upgrades to the new version and fails
- canary0.failDeployment(systemTest);
- canary0.abortJob(stagingTest);
- tester.controllerTester().computeVersionStatus();
- assertEquals(
- Confidence.broken, confidence(tester.controller(), version1), "One canary failed: Broken");
-
- // New version is released
- Version version2 = new Version("6.4");
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().maintain();
- assertEquals(Confidence.broken, confidence(tester.controller(), version1),
- "Confidence remains unchanged for version1 until app overrides old tests: Broken");
- assertEquals(Confidence.low, confidence(tester.controller(), version2),
- "Confidence defaults to low for version with no applications");
- assertEquals(version2, canary0.instance().change().platform().orElseThrow());
-
- canary0.failDeployment(systemTest);
- canary0.abortJob(stagingTest);
- tester.controllerTester().computeVersionStatus();
- assertFalse(tester.controller().readVersionStatus().versions().stream().anyMatch(version -> version.versionNumber().equals(version1)),
- "Previous version should be forgotten, as canary only had test jobs run on it");
-
- // App succeeds with tests, but fails production deployment
- canary0.runJob(systemTest)
- .runJob(stagingTest)
- .failDeployment(productionUsWest1);
-
- assertEquals(
- Confidence.broken, confidence(tester.controller(), version2), "One canary failed: Broken");
-
- // A new version is released, and the app again fails production deployment.
- Version version3 = new Version("6.5");
- tester.controllerTester().upgradeSystem(version3);
- tester.upgrader().maintain();
- assertEquals(Confidence.broken, confidence(tester.controller(), version2),
- "Confidence remains unchanged for version2: Broken");
- assertEquals(Confidence.low, confidence(tester.controller(), version3),
- "Confidence defaults to low for version with no applications");
- assertEquals(version3, canary0.instance().change().platform().orElseThrow());
-
- canary0.runJob(systemTest)
- .runJob(stagingTest)
- .failDeployment(productionUsWest1);
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.broken, confidence(tester.controller(), version2),
- "Confidence remains unchanged for version2: Broken");
- assertEquals(Confidence.broken, confidence(tester.controller(), version3),
- "Canary broken, so confidence for version3: Broken");
-
- // App succeeds production deployment, clearing failure on version2
- canary0.runJob(productionUsWest1);
- tester.controllerTester().computeVersionStatus();
- assertFalse(tester.controller().readVersionStatus().versions().stream().anyMatch(version -> version.versionNumber().equals(version2)),
- "Previous version should be forgotten, as canary only had test jobs run on it");
- assertEquals(Confidence.low, confidence(tester.controller(), version3),
- "Canary OK, but not done upgrading, so confidence for version3: Low");
- }
-
- @Test
- public void testConfidenceOverride() {
- DeploymentTester tester = new DeploymentTester();
- Version version0 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version0);
-
- // Create and deploy application on current version
- var app = tester.newDeploymentContext("tenant1", "app1", "default")
- .submit()
- .deploy();
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.high, confidence(tester.controller(), version0));
-
- // Override confidence
- tester.upgrader().overrideConfidence(version0, Confidence.broken);
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.broken, confidence(tester.controller(), version0));
-
- // New version is released and application upgrades
- Version version1 = new Version("6.3");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- app.deployPlatform(version1);
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.high, confidence(tester.controller(), version1));
-
- // Stale override was removed
- assertFalse( tester.controller().curator().readConfidenceOverrides()
- .containsKey(version0),
- "Stale override removed");
- }
-
- @Test
- public void testCommitDetailsPreservation() {
- HostName controller1 = HostName.of("controller-1");
- HostName controller2 = HostName.of("controller-2");
- HostName controller3 = HostName.of("controller-3");
- MockCuratorDb db = new MockCuratorDb(Stream.of(controller1, controller2, controller3)
- .map(hostName -> hostName.value() + ":2222")
- .collect(Collectors.joining(",")));
- DeploymentTester tester = new DeploymentTester(new ControllerTester(db));
-
- // Commit details are set for initial version
- var version0 = tester.controllerTester().nextVersion();
- var commitSha0 = "badc0ffee";
- var commitDate0 = Instant.EPOCH;
- tester.controllerTester().upgradeSystem(version0);
- assertEquals(version0, tester.controller().readVersionStatus().systemVersion().get().versionNumber());
- assertEquals(commitSha0, tester.controller().readVersionStatus().systemVersion().get().releaseCommit());
- assertEquals(commitDate0, tester.controller().readVersionStatus().systemVersion().get().committedAt());
-
- // Deploy app on version0 to keep computing statistics for that version
- tester.newDeploymentContext().submit().deploy();
-
- // Commit details are updated for new version
- var version1 = tester.controllerTester().nextVersion();
- var commitSha1 = "deadbeef";
- var commitDate1 = Instant.ofEpochMilli(123);
- tester.controllerTester().upgradeController(version1, commitSha1, commitDate1);
- tester.controllerTester().upgradeSystemApplications(version1);
- assertEquals(version1, tester.controller().readVersionStatus().systemVersion().get().versionNumber());
- assertEquals(commitSha1, tester.controller().readVersionStatus().systemVersion().get().releaseCommit());
- assertEquals(commitDate1, tester.controller().readVersionStatus().systemVersion().get().committedAt());
-
- // Commit details for previous version are preserved
- assertEquals(commitSha0, tester.controller().readVersionStatus().version(version0).releaseCommit());
- assertEquals(commitDate0, tester.controller().readVersionStatus().version(version0).committedAt());
- }
-
- @Test
- public void testConfidenceChangeRespectsTimeWindow() {
- DeploymentTester tester = new DeploymentTester().atMondayMorning();
- // Canaries and normal application deploys on initial version
- Version version0 = Version.fromString("7.1");
- tester.controllerTester().upgradeSystem(version0);
- var canary0 = tester.newDeploymentContext("tenant1", "canary0", "default")
- .submit(applicationPackage("canary"))
- .deploy();
- var canary1 = tester.newDeploymentContext("tenant1", "canary1", "default")
- .submit(applicationPackage("canary"))
- .deploy();
- var default0 = tester.newDeploymentContext("tenant1", "default0", "default")
- .submit(applicationPackage("default"))
- .deploy();
- tester.controllerTester().computeVersionStatus();
- assertSame(Confidence.high, tester.controller().readVersionStatus().version(version0).confidence());
-
- // System and canary0 is upgraded within allowed time window
- Version version1 = Version.fromString("7.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
- canary0.deployPlatform(version1);
- tester.controllerTester().computeVersionStatus();
- assertSame(Confidence.low, tester.controller().readVersionStatus().version(version1).confidence());
-
- // canary1 breaks just outside allowed upgrade window
- assertEquals(12, tester.controllerTester().hourOfDayAfter(Duration.ofHours(7)));
- canary1.failDeployment(systemTest);
- tester.controllerTester().computeVersionStatus();
- assertSame(Confidence.broken, tester.controller().readVersionStatus().version(version1).confidence());
-
- // Second canary is fixed later in the day. All canaries are now fixed, but confidence is not raised as we're
- // outside the allowed time window
- assertEquals(20, tester.controllerTester().hourOfDayAfter(Duration.ofHours(8)));
- canary1.deployPlatform(version1);
- tester.controllerTester().computeVersionStatus();
- assertSame(Confidence.broken, tester.controller().readVersionStatus().version(version1).confidence());
-
- // Early morning arrives, confidence is raised and normal application upgrades
- assertEquals(5, tester.controllerTester().hourOfDayAfter(Duration.ofHours(9)));
- tester.controllerTester().computeVersionStatus();
- assertSame(Confidence.normal, tester.controller().readVersionStatus().version(version1).confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
- default0.deployPlatform(version1);
-
- // Another version is released. System and canaries upgrades late, confidence stays low
- Version version2 = Version.fromString("7.3");
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().maintain();
- assertEquals(14, tester.controllerTester().hourOfDayAfter(Duration.ofHours(9)));
- canary0.deployPlatform(version2);
- canary1.deployPlatform(version2);
- tester.controllerTester().computeVersionStatus();
- assertSame(Confidence.low, tester.controller().readVersionStatus().version(version2).confidence());
-
- // Confidence override takes precedence over time window constraints
- tester.upgrader().overrideConfidence(version2, Confidence.normal);
- tester.controllerTester().computeVersionStatus();
- assertSame(Confidence.normal, tester.controller().readVersionStatus().version(version2).confidence());
- tester.upgrader().overrideConfidence(version2, Confidence.low);
- tester.controllerTester().computeVersionStatus();
- assertSame(Confidence.low, tester.controller().readVersionStatus().version(version2).confidence());
- tester.upgrader().removeConfidenceOverride(version2);
-
- // Next morning arrives, confidence is raised and normal application upgrades
- assertEquals(7, tester.controllerTester().hourOfDayAfter(Duration.ofHours(17)));
- tester.controllerTester().computeVersionStatus();
- assertSame(Confidence.normal, tester.controller().readVersionStatus().version(version2).confidence());
- tester.upgrader().maintain();
- tester.triggerJobs();
- default0.deployPlatform(version2);
- }
-
- @Test
- public void testStatusIncludesIncompleteUpgrades() {
- var tester = new DeploymentTester().atMondayMorning();
- var version0 = Version.fromString("7.1");
-
- // Application deploys on initial version
- tester.controllerTester().upgradeSystem(version0);
- var context = tester.newDeploymentContext("tenant1", "default0", "default");
- context.submit(applicationPackage("default")).deploy();
-
- // System is upgraded and application starts upgrading to next version
- var version1 = Version.fromString("7.2");
- tester.controllerTester().upgradeSystem(version1);
- tester.upgrader().maintain();
-
- // Upgrade of prod zone fails
- context.runJob(systemTest)
- .runJob(stagingTest)
- .failDeployment(productionUsWest1);
- tester.controllerTester().computeVersionStatus();
- for (var version : List.of(version0, version1)) {
- assertOnVersion(version, context.instanceId(), tester);
- }
-
- // System is upgraded and application starts upgrading to next version
- var version2 = Version.fromString("7.3");
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().maintain();
-
- // Upgrade of prod zone fails again, application is now potentially on 3 different versions:
- // 1 completed upgrade + 2 failed
- context.runJob(systemTest)
- .runJob(stagingTest)
- .failDeployment(productionUsWest1);
- tester.controllerTester().computeVersionStatus();
- for (var version : List.of(version0, version1, version2)) {
- assertOnVersion(version, context.instanceId(), tester);
- }
-
- // Upgrade succeeds
- context.deployPlatform(version2);
- tester.controllerTester().computeVersionStatus();
- assertEquals(1, tester.controller().readVersionStatus().versions().size());
- assertOnVersion(version2, context.instanceId(), tester);
-
- // System is upgraded and application starts upgrading to next version
- var version3 = Version.fromString("7.4");
- tester.controllerTester().upgradeSystem(version3);
- tester.upgrader().maintain();
-
- // Upgrade of prod zone fails again. Upgrades that failed before the most recent success are not counted
- context.runJob(systemTest)
- .runJob(stagingTest)
- .failDeployment(productionUsWest1);
- tester.controllerTester().computeVersionStatus();
- assertEquals(2, tester.controller().readVersionStatus().versions().size());
- for (var version : List.of(version2, version3)) {
- assertOnVersion(version, context.instanceId(), tester);
- }
- }
-
- @Test
- void testPinnedAppsAreIgnoredForIncreasingConfidenceWhenLessThanHalfArePinned() {
- DeploymentContext canaries[] = new DeploymentContext[3];
- DeploymentContext defaults[] = new DeploymentContext[3];
- DeploymentTester tester = new DeploymentTester().atMondayMorning();
- Version version1 = new Version("6.2");
- tester.controllerTester().upgradeSystem(version1);
-
- for (int i = 0; i < 3; i++) {
- canaries[i] = tester.newDeploymentContext("t" + i, "a", "default");
- canaries[i].submit(canaryApplicationPackage).deploy();
- defaults[i] = tester.newDeploymentContext("t" + i, "b", "default");
- defaults[i].submit(defaultApplicationPackage).deploy();
- }
-
- assertEquals(Confidence.high, confidence(tester.controller(), version1));
-
- // All apps are pinned to version1, and then version2 releases. Initial confidence is low.
- for (int i = 0; i < 3; i++) {
- tester.deploymentTrigger().forceChange(canaries[i].instanceId(), Change.empty().withPlatformPin());
- tester.deploymentTrigger().forceChange(defaults[i].instanceId(), Change.empty().withPlatformPin());
- }
- Version version2 = new Version("6.3");
- tester.controllerTester().upgradeSystem(version2);
- tester.upgrader().maintain();
- tester.triggerJobs();
- assertEquals(List.of(), tester.jobs().active());
- assertEquals(Confidence.low, confidence(tester.controller(), version2));
-
- // One canary and one default are unpinned and upgrade. Confidence remains low,
- // as more than half the apps are pinned, and less tan 100%/90% have upgraded.
- tester.deploymentTrigger().cancelChange(canaries[0].instanceId(), ChangesToCancel.ALL);
- tester.deploymentTrigger().forceChange(canaries[0].instanceId(), Change.of(version2));
- canaries[0].deployPlatform(version2);
- tester.deploymentTrigger().cancelChange(defaults[0].instanceId(), ChangesToCancel.ALL);
- tester.deploymentTrigger().forceChange(defaults[0].instanceId(), Change.of(version2));
- defaults[0].deployPlatform(version2);
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.low, confidence(tester.controller(), version2));
-
- // All apps are unpinned, and another canary and default upgrade. Confidence still remains low,
- // as less than half of the unpinned apps have upgraded.
- tester.deploymentTrigger().cancelChange(canaries[1].instanceId(), ChangesToCancel.ALL);
- tester.deploymentTrigger().cancelChange(canaries[2].instanceId(), ChangesToCancel.ALL);
- tester.deploymentTrigger().forceChange(canaries[1].instanceId(), Change.of(version2));
- tester.deploymentTrigger().cancelChange(defaults[1].instanceId(), ChangesToCancel.ALL);
- tester.deploymentTrigger().cancelChange(defaults[2].instanceId(), ChangesToCancel.ALL);
- tester.deploymentTrigger().forceChange(defaults[1].instanceId(), Change.of(version2));
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.low, confidence(tester.controller(), version2));
-
- // The second canary upgrades while the last is unpinned.
- canaries[1].deployPlatform(version2);
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.low, confidence(tester.controller(), version2));
-
- // When the last remaining canary is pinned, less than half are pinned, and all have upgraded,
- // so confidence finally increases to normal.
- tester.deploymentTrigger().forceChange(canaries[2].instanceId(), Change.empty().withPlatformPin());
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.normal, confidence(tester.controller(), version2));
-
- // The second default upgrades while the last is unpinned.
- defaults[1].deployPlatform(version2);
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.normal, confidence(tester.controller(), version2));
-
- // When the last remaining default is pinned, less than half are pinned, and more than 90% have upgraded,
- // so confidence increases to high.
- tester.deploymentTrigger().forceChange(defaults[2].instanceId(), Change.empty().withPlatformPin());
- tester.controllerTester().computeVersionStatus();
- assertEquals(Confidence.high, confidence(tester.controller(), version2));
- }
-
- private void assertOnVersion(Version version, ApplicationId instance, DeploymentTester tester) {
- var vespaVersion = tester.controller().readVersionStatus().version(version);
- assertNotNull(vespaVersion, "Statistics for version " + version + " exist");
- var statistics = DeploymentStatistics.compute(List.of(version), tester.deploymentStatuses()).get(0);
- assertTrue(
- Stream.of(statistics.productionSuccesses(), statistics.failingUpgrades(), statistics.runningUpgrade())
- .anyMatch(runs -> runs.stream().anyMatch(run -> run.id().application().equals(instance))), "Application is on version " + version);
- }
-
- private static void writeControllerVersion(HostName hostname, Version version, CuratorDb db) {
- db.writeControllerVersion(hostname, new ControllerVersion(version, "badc0ffee", Instant.EPOCH));
- }
-
- private Confidence confidence(Controller controller, Version version) {
- return controller.readVersionStatus().versions().stream()
- .filter(v -> v.versionNumber().equals(version))
- .findFirst()
- .map(VespaVersion::confidence)
- .orElseThrow(() -> new IllegalArgumentException("Expected to find version: " + version));
- }
-
- private static ApplicationPackage applicationPackage(String upgradePolicy, int majorVersion) {
- return new ApplicationPackageBuilder().upgradePolicy(upgradePolicy)
- .region("us-west-1")
- .region("us-east-3")
- .majorVersion(majorVersion)
- .build();
- }
-
- private static final ApplicationPackage canaryApplicationPackage =
- new ApplicationPackageBuilder().upgradePolicy("canary")
- .region("us-west-1")
- .region("us-east-3")
- .build();
-
- private static final ApplicationPackage defaultApplicationPackage =
- new ApplicationPackageBuilder().upgradePolicy("default")
- .region("us-west-1")
- .region("us-east-3")
- .build();
-
- private static final ApplicationPackage conservativeApplicationPackage =
- new ApplicationPackageBuilder().upgradePolicy("conservative")
- .region("us-west-1")
- .region("us-east-3")
- .build();
-
- /** Returns empty prebuilt applications for efficiency */
- private ApplicationPackage applicationPackage(String upgradePolicy) {
- return switch (upgradePolicy) {
- case "canary" -> canaryApplicationPackage;
- case "default" -> defaultApplicationPackage;
- case "conservative" -> conservativeApplicationPackage;
- default -> throw new IllegalArgumentException("No upgrade policy '" + upgradePolicy + "'");
- };
- }
-
-}
diff --git a/controller-server/src/test/resources/application-packages/changed-deployment-xml.zip b/controller-server/src/test/resources/application-packages/changed-deployment-xml.zip
deleted file mode 100644
index e6482904b22..00000000000
--- a/controller-server/src/test/resources/application-packages/changed-deployment-xml.zip
+++ /dev/null
Binary files differ
diff --git a/controller-server/src/test/resources/application-packages/changed-services-xml.zip b/controller-server/src/test/resources/application-packages/changed-services-xml.zip
deleted file mode 100644
index e11b1ef162e..00000000000
--- a/controller-server/src/test/resources/application-packages/changed-services-xml.zip
+++ /dev/null
Binary files differ
diff --git a/controller-server/src/test/resources/application-packages/include-absolute.zip b/controller-server/src/test/resources/application-packages/include-absolute.zip
deleted file mode 100644
index 49c99ff5da9..00000000000
--- a/controller-server/src/test/resources/application-packages/include-absolute.zip
+++ /dev/null
Binary files differ
diff --git a/controller-server/src/test/resources/application-packages/include-parent.zip b/controller-server/src/test/resources/application-packages/include-parent.zip
deleted file mode 100644
index 8702b512c98..00000000000
--- a/controller-server/src/test/resources/application-packages/include-parent.zip
+++ /dev/null
Binary files differ
diff --git a/controller-server/src/test/resources/application-packages/original.zip b/controller-server/src/test/resources/application-packages/original.zip
deleted file mode 100644
index cabac1999c3..00000000000
--- a/controller-server/src/test/resources/application-packages/original.zip
+++ /dev/null
Binary files differ
diff --git a/controller-server/src/test/resources/application-packages/similar-deployment-xml.zip b/controller-server/src/test/resources/application-packages/similar-deployment-xml.zip
deleted file mode 100644
index 67c38c344c0..00000000000
--- a/controller-server/src/test/resources/application-packages/similar-deployment-xml.zip
+++ /dev/null
Binary files differ
diff --git a/controller-server/src/test/resources/application-packages/with-certificate.zip b/controller-server/src/test/resources/application-packages/with-certificate.zip
deleted file mode 100644
index 1540b96c7ef..00000000000
--- a/controller-server/src/test/resources/application-packages/with-certificate.zip
+++ /dev/null
Binary files differ
diff --git a/controller-server/src/test/resources/config-models/cd/config-models-cd.xml b/controller-server/src/test/resources/config-models/cd/config-models-cd.xml
deleted file mode 100644
index 3562d7fd997..00000000000
--- a/controller-server/src/test/resources/config-models/cd/config-models-cd.xml
+++ /dev/null
@@ -1,26 +0,0 @@
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
- <component id='VespaModelFactory.8.218.31' class='com.yahoo.vespa.model.VespaModelFactory' bundle='config-model-fat-amended:8.218.31' />
- <component id='YahooAdminModelAmender.8.218.31' class='com.yahoo.vespa.model.admin.amender.YahooAdminModelAmender' bundle='config-model-fat-amended:8.218.31' />
- <component id='YahooModelValidator.8.218.31' class='com.yahoo.vespa.model.application.validation.YahooModelValidator' bundle='config-model-fat-amended:8.218.31' />
- <component id='YahooContainerModelAmender.8.218.31' class='com.yahoo.vespa.model.container.amender.YahooContainerModelAmender' bundle='config-model-fat-amended:8.218.31' />
- <component id='YahooContentContainerModelAmender.8.218.31' class='com.yahoo.vespa.model.container.amender.YahooContentContainerModelAmender' bundle='config-model-fat-amended:8.218.31' />
- <component id='VespaModelFactory.8.222.21' class='com.yahoo.vespa.model.VespaModelFactory' bundle='config-model-fat-amended:8.222.21' />
- <component id='YahooAdminModelAmender.8.222.21' class='com.yahoo.vespa.model.admin.amender.YahooAdminModelAmender' bundle='config-model-fat-amended:8.222.21' />
- <component id='YahooModelValidator.8.222.21' class='com.yahoo.vespa.model.application.validation.YahooModelValidator' bundle='config-model-fat-amended:8.222.21' />
- <component id='YahooContainerModelAmender.8.222.21' class='com.yahoo.vespa.model.container.amender.YahooContainerModelAmender' bundle='config-model-fat-amended:8.222.21' />
- <component id='YahooContentContainerModelAmender.8.222.21' class='com.yahoo.vespa.model.container.amender.YahooContentContainerModelAmender' bundle='config-model-fat-amended:8.222.21' />
- <component id='VespaModelFactory.8.222.22' class='com.yahoo.vespa.model.VespaModelFactory' bundle='config-model-fat-amended:8.222.22' />
- <component id='YahooAdminModelAmender.8.222.22' class='com.yahoo.vespa.model.admin.amender.YahooAdminModelAmender' bundle='config-model-fat-amended:8.222.22' />
- <component id='YahooModelValidator.8.222.22' class='com.yahoo.vespa.model.application.validation.YahooModelValidator' bundle='config-model-fat-amended:8.222.22' />
- <component id='YahooContainerModelAmender.8.222.22' class='com.yahoo.vespa.model.container.amender.YahooContainerModelAmender' bundle='config-model-fat-amended:8.222.22' />
- <component id='YahooContentContainerModelAmender.8.222.22' class='com.yahoo.vespa.model.container.amender.YahooContentContainerModelAmender' bundle='config-model-fat-amended:8.222.22' />
- <component id='VespaModelFactory.8.223.1' class='com.yahoo.vespa.model.VespaModelFactory' bundle='config-model-fat-amended:8.223.1' />
- <component id='YahooAdminModelAmender.8.223.1' class='com.yahoo.vespa.model.admin.amender.YahooAdminModelAmender' bundle='config-model-fat-amended:8.223.1' />
- <component id='YahooModelValidator.8.223.1' class='com.yahoo.vespa.model.application.validation.YahooModelValidator' bundle='config-model-fat-amended:8.223.1' />
- <component id='YahooContainerModelAmender.8.223.1' class='com.yahoo.vespa.model.container.amender.YahooContainerModelAmender' bundle='config-model-fat-amended:8.223.1' />
- <component id='YahooContentContainerModelAmender.8.223.1' class='com.yahoo.vespa.model.container.amender.YahooContentContainerModelAmender' bundle='config-model-fat-amended:8.223.1' />
- <component id='VespaModelFactory.8.223.2' class='com.yahoo.vespa.model.VespaModelFactory' bundle='config-model-fat-amended:8.223.2' />
- <component id='YahooAdminModelAmender.8.223.2' class='com.yahoo.vespa.model.admin.amender.YahooAdminModelAmender' bundle='config-model-fat-amended:8.223.2' />
- <component id='YahooModelValidator.8.223.2' class='com.yahoo.vespa.model.application.validation.YahooModelValidator' bundle='config-model-fat-amended:8.223.2' />
- <component id='YahooContainerModelAmender.8.223.2' class='com.yahoo.vespa.model.container.amender.YahooContainerModelAmender' bundle='config-model-fat-amended:8.223.2' />
- <component id='YahooContentContainerModelAmender.8.223.2' class='com.yahoo.vespa.model.container.amender.YahooContentContainerModelAmender' bundle='config-model-fat-amended:8.223.2' />
diff --git a/controller-server/src/test/resources/config-models/cd/config-models-main.xml b/controller-server/src/test/resources/config-models/cd/config-models-main.xml
deleted file mode 100644
index 55c0e8b8bb6..00000000000
--- a/controller-server/src/test/resources/config-models/cd/config-models-main.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-<!-- Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
- <component id='VespaModelFactory.8.218.31' class='com.yahoo.vespa.model.VespaModelFactory' bundle='config-model-fat-amended:8.218.31' />
- <component id='YahooAdminModelAmender.8.218.31' class='com.yahoo.vespa.model.admin.amender.YahooAdminModelAmender' bundle='config-model-fat-amended:8.218.31' />
- <component id='YahooModelValidator.8.218.31' class='com.yahoo.vespa.model.application.validation.YahooModelValidator' bundle='config-model-fat-amended:8.218.31' />
- <component id='YahooContainerModelAmender.8.218.31' class='com.yahoo.vespa.model.container.amender.YahooContainerModelAmender' bundle='config-model-fat-amended:8.218.31' />
- <component id='YahooContentContainerModelAmender.8.218.31' class='com.yahoo.vespa.model.container.amender.YahooContentContainerModelAmender' bundle='config-model-fat-amended:8.218.31' />
- <component id='VespaModelFactory.8.219.19' class='com.yahoo.vespa.model.VespaModelFactory' bundle='config-model-fat-amended:8.219.19' />
- <component id='YahooAdminModelAmender.8.219.19' class='com.yahoo.vespa.model.admin.amender.YahooAdminModelAmender' bundle='config-model-fat-amended:8.219.19' />
- <component id='YahooModelValidator.8.219.19' class='com.yahoo.vespa.model.application.validation.YahooModelValidator' bundle='config-model-fat-amended:8.219.19' />
- <component id='YahooContainerModelAmender.8.219.19' class='com.yahoo.vespa.model.container.amender.YahooContainerModelAmender' bundle='config-model-fat-amended:8.219.19' />
- <component id='YahooContentContainerModelAmender.8.219.19' class='com.yahoo.vespa.model.container.amender.YahooContentContainerModelAmender' bundle='config-model-fat-amended:8.219.19' />
- <component id='VespaModelFactory.8.220.15' class='com.yahoo.vespa.model.VespaModelFactory' bundle='config-model-fat-amended:8.220.15' />
- <component id='YahooAdminModelAmender.8.220.15' class='com.yahoo.vespa.model.admin.amender.YahooAdminModelAmender' bundle='config-model-fat-amended:8.220.15' />
- <component id='YahooModelValidator.8.220.15' class='com.yahoo.vespa.model.application.validation.YahooModelValidator' bundle='config-model-fat-amended:8.220.15' />
- <component id='YahooContainerModelAmender.8.220.15' class='com.yahoo.vespa.model.container.amender.YahooContainerModelAmender' bundle='config-model-fat-amended:8.220.15' />
- <component id='YahooContentContainerModelAmender.8.220.15' class='com.yahoo.vespa.model.container.amender.YahooContentContainerModelAmender' bundle='config-model-fat-amended:8.222.22' />
diff --git a/controller-server/src/test/resources/config-models/empty/config-models-cd.xml b/controller-server/src/test/resources/config-models/empty/config-models-cd.xml
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/controller-server/src/test/resources/config-models/empty/config-models-cd.xml
+++ /dev/null
diff --git a/controller-server/src/test/resources/config-models/empty/config-models-main.xml b/controller-server/src/test/resources/config-models/empty/config-models-main.xml
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/controller-server/src/test/resources/config-models/empty/config-models-main.xml
+++ /dev/null
diff --git a/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.json b/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.json
deleted file mode 100644
index a71fd812de9..00000000000
--- a/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.json
+++ /dev/null
@@ -1,37 +0,0 @@
-{
- "start": 1619301600000,
- "end": 1623161217471,
- "executionGraph": [
- {
- "id": "q1_m1",
- "type": "TimeSeriesDataSource",
- "metric": {
- "type": "MetricLiteral",
- "metric": "Vespa.vespa.distributor.vds.distributor.docsstored.average"
- },
- "sourceId": null,
- "fetchLast": false,
- "filter": {
- "type": "Chain",
- "op": "AND",
- "filters": [
- {
- "type": "TagValueLiteralOr",
- "filter": "tenant1.application1.instance1",
- "tagKey": "applicationId"
- },
- {
- "type": "TagValueLiteralOr",
- "filter": "public",
- "tagKey": "system"
- },
- {
- "type": "TagValueRegex",
- "filter": "^(tenant2|tenant3)\\..*",
- "tagKey": "applicationId"
- }
- ]
- }
- }
- ]
-}
diff --git a/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.operator.json b/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.operator.json
deleted file mode 100644
index babf3219c6a..00000000000
--- a/controller-server/src/test/resources/horizon/filter-in-execution-graph.expected.operator.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "start": 1619301600000,
- "end": 1623161217471,
- "executionGraph": [
- {
- "id": "q1_m1",
- "type": "TimeSeriesDataSource",
- "metric": {
- "type": "MetricLiteral",
- "metric": "Vespa.vespa.distributor.vds.distributor.docsstored.average"
- },
- "sourceId": null,
- "fetchLast": false,
- "filter": {
- "type": "Chain",
- "op": "AND",
- "filters": [
- {
- "type": "TagValueLiteralOr",
- "filter": "tenant1.application1.instance1",
- "tagKey": "applicationId"
- },
- {
- "type": "TagValueLiteralOr",
- "filter": "public",
- "tagKey": "system"
- }
- ]
- }
- }
- ]
-}
diff --git a/controller-server/src/test/resources/horizon/filter-in-execution-graph.json b/controller-server/src/test/resources/horizon/filter-in-execution-graph.json
deleted file mode 100644
index 6a2512c3642..00000000000
--- a/controller-server/src/test/resources/horizon/filter-in-execution-graph.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
- "start": 1619301600000,
- "end": 1623161217471,
- "executionGraph": [
- {
- "id": "q1_m1",
- "type": "TimeSeriesDataSource",
- "metric": {
- "type": "MetricLiteral",
- "metric": "Vespa.vespa.distributor.vds.distributor.docsstored.average"
- },
- "sourceId": null,
- "fetchLast": false,
- "filter": {
- "type": "TagValueLiteralOr",
- "filter": "tenant1.application1.instance1",
- "tagKey": "applicationId"
- }
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/resources/horizon/filters-complex.expected.json b/controller-server/src/test/resources/horizon/filters-complex.expected.json
deleted file mode 100644
index 30ee6a28d7e..00000000000
--- a/controller-server/src/test/resources/horizon/filters-complex.expected.json
+++ /dev/null
@@ -1,56 +0,0 @@
-{
- "start": 1623080040000,
- "end": 1623166440000,
- "executionGraph": [
- {
- "id": "q1_m1",
- "type": "TimeSeriesDataSource",
- "metric": {
- "type": "MetricLiteral",
- "metric": "Vespa.vespa.container.documents_covered.count"
- },
- "sourceId": null,
- "fetchLast": false,
- "filterId": "filter-ni8"
- }
- ],
- "filters": [
- {
- "filter": {
- "type": "Chain",
- "op": "AND",
- "filters": [
- {
- "type": "NOT",
- "filter": {
- "type": "TagValueLiteralOr",
- "filter": "tenant1.app1.instance1",
- "tagKey": "applicationId"
- }
- },
- {
- "type": "TagValueLiteralOr",
- "filter": "public",
- "tagKey": "system"
- },
- {
- "type": "TagValueRegex",
- "filter": "^(tenant2)\\..*",
- "tagKey": "applicationId"
- }
- ]
- },
- "id": "filter-ni8"
- }
- ],
- "serdesConfigs": [
- {
- "id": "JsonV3QuerySerdes",
- "filter": [
- "summarizer"
- ]
- }
- ],
- "logLevel": "ERROR",
- "cacheMode": null
-}
diff --git a/controller-server/src/test/resources/horizon/filters-complex.json b/controller-server/src/test/resources/horizon/filters-complex.json
deleted file mode 100644
index e21fa61128a..00000000000
--- a/controller-server/src/test/resources/horizon/filters-complex.json
+++ /dev/null
@@ -1,46 +0,0 @@
-{
- "start": 1623080040000,
- "end": 1623166440000,
- "executionGraph": [
- {
- "id": "q1_m1",
- "type": "TimeSeriesDataSource",
- "metric": {
- "type": "MetricLiteral",
- "metric": "Vespa.vespa.container.documents_covered.count"
- },
- "sourceId": null,
- "fetchLast": false,
- "filterId": "filter-ni8"
- }
- ],
- "filters": [
- {
- "filter": {
- "type": "Chain",
- "op": "AND",
- "filters": [
- {
- "type": "NOT",
- "filter": {
- "type": "TagValueLiteralOr",
- "filter": "tenant1.app1.instance1",
- "tagKey": "applicationId"
- }
- }
- ]
- },
- "id": "filter-ni8"
- }
- ],
- "serdesConfigs": [
- {
- "id": "JsonV3QuerySerdes",
- "filter": [
- "summarizer"
- ]
- }
- ],
- "logLevel": "ERROR",
- "cacheMode": null
-}
diff --git a/controller-server/src/test/resources/horizon/filters-meta-query.expected.json b/controller-server/src/test/resources/horizon/filters-meta-query.expected.json
deleted file mode 100644
index 6c8cab217fa..00000000000
--- a/controller-server/src/test/resources/horizon/filters-meta-query.expected.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "from": 0,
- "to": 1,
- "order": "ASCENDING",
- "type": "TAG_KEYS_AND_VALUES",
- "source": "",
- "aggregationSize": 1000,
- "queries": [
- {
- "id": "id-0",
- "namespace": "Vespa",
- "filter": {
- "type": "Chain",
- "filters": [
- {
- "type": "TagValueRegex",
- "filter": ".*",
- "tagKey": "applicationId"
- },
- {
- "type": "MetricLiteral",
- "metric": "vespa.distributor.vds.distributor.docsstored.average|vespa.searchnode.content.proton.resource_usage.disk.average|vespa.searchnode.content.proton.resource_usage.memory.average|vespa.container.peak_qps.max"
- },
- {
- "type": "TagValueLiteralOr",
- "filter": "public",
- "tagKey": "system"
- },
- {
- "type": "TagValueRegex",
- "filter": "^(tenant2|tenant3)\\..*",
- "tagKey": "applicationId"
- }
- ]
- }
- }
- ],
- "aggregationField": "applicationId"
-}
diff --git a/controller-server/src/test/resources/horizon/filters-meta-query.json b/controller-server/src/test/resources/horizon/filters-meta-query.json
deleted file mode 100644
index ed59bef5eaa..00000000000
--- a/controller-server/src/test/resources/horizon/filters-meta-query.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "from": 0,
- "to": 1,
- "order": "ASCENDING",
- "type": "TAG_KEYS_AND_VALUES",
- "source": "",
- "aggregationSize": 1000,
- "queries": [
- {
- "id": "id-0",
- "namespace": "Vespa",
- "filter": {
- "type": "Chain",
- "filters": [
- {
- "type": "TagValueRegex",
- "filter": ".*",
- "tagKey": "applicationId"
- },
- {
- "type": "MetricLiteral",
- "metric": "vespa.distributor.vds.distributor.docsstored.average|vespa.searchnode.content.proton.resource_usage.disk.average|vespa.searchnode.content.proton.resource_usage.memory.average|vespa.container.peak_qps.max"
- }
- ]
- }
- }
- ],
- "aggregationField": "applicationId"
-} \ No newline at end of file
diff --git a/controller-server/src/test/resources/horizon/no-filters.expected.json b/controller-server/src/test/resources/horizon/no-filters.expected.json
deleted file mode 100644
index 35decea21db..00000000000
--- a/controller-server/src/test/resources/horizon/no-filters.expected.json
+++ /dev/null
@@ -1,32 +0,0 @@
-{
- "start": 1619301600000,
- "end": 1623161217471,
- "executionGraph": [
- {
- "id": "q1_m1",
- "type": "TimeSeriesDataSource",
- "metric": {
- "type": "MetricLiteral",
- "metric": "Vespa.vespa.distributor.vds.distributor.docsstored.average"
- },
- "sourceId": null,
- "fetchLast": false,
- "filter": {
- "type": "Chain",
- "op": "AND",
- "filters": [
- {
- "type": "TagValueLiteralOr",
- "filter": "public",
- "tagKey": "system"
- },
- {
- "type": "TagValueRegex",
- "filter": "^(tenant2|tenant3)\\..*",
- "tagKey": "applicationId"
- }
- ]
- }
- }
- ]
-}
diff --git a/controller-server/src/test/resources/horizon/no-filters.json b/controller-server/src/test/resources/horizon/no-filters.json
deleted file mode 100644
index 3ff80feba02..00000000000
--- a/controller-server/src/test/resources/horizon/no-filters.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "start": 1619301600000,
- "end": 1623161217471,
- "executionGraph": [
- {
- "id": "q1_m1",
- "type": "TimeSeriesDataSource",
- "metric": {
- "type": "MetricLiteral",
- "metric": "Vespa.vespa.distributor.vds.distributor.docsstored.average"
- },
- "sourceId": null,
- "fetchLast": false
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/resources/job.json b/controller-server/src/test/resources/job.json
deleted file mode 100644
index 845566867b7..00000000000
--- a/controller-server/src/test/resources/job.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "duration": 600000,
- "causes": [
- {
- "upstreamBuild": 123,
- "upstreamProject": "2-v3-job-parent"
- }
- ]
-}
diff --git a/controller-server/src/test/resources/mail/notification.html b/controller-server/src/test/resources/mail/notification.html
deleted file mode 100644
index 2a0edeea7e1..00000000000
--- a/controller-server/src/test/resources/mail/notification.html
+++ /dev/null
@@ -1,649 +0,0 @@
-<!DOCTYPE html>
-<html
- xmlns="http://www.w3.org/1999/xhtml"
- xmlns:v="urn:schemas-microsoft-com:vml"
- xmlns:o="urn:schemas-microsoft-com:office:office"
->
- <head>
- <title></title>
- <!--[if !mso]><!-->
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
- <!--<![endif]-->
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <style type="text/css">
- #outlook a {
- padding: 0;
- }
-
- body {
- margin: 0;
- padding: 0;
- -webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
- }
-
- table,
- td {
- border-collapse: collapse;
- mso-table-lspace: 0pt;
- mso-table-rspace: 0pt;
- }
-
- img {
- border: 0;
- height: auto;
- line-height: 100%;
- outline: none;
- text-decoration: none;
- -ms-interpolation-mode: bicubic;
- }
-
- p {
- display: block;
- margin: 13px 0;
- }
- </style>
- <!--[if mso]>
- <noscript>
- <xml>
- <o:OfficeDocumentSettings>
- <o:AllowPNG />
- <o:PixelsPerInch>96</o:PixelsPerInch>
- </o:OfficeDocumentSettings>
- </xml>
- </noscript>
- <![endif]-->
- <!--[if lte mso 11]>
- <style type="text/css">
- .mj-outlook-group-fix {
- width: 100% !important;
- }
- </style>
- <![endif]-->
- <!--[if !mso]><!-->
- <link
- href="https://fonts.googleapis.com/css?family=Open Sans"
- rel="stylesheet"
- type="text/css"
- />
- <style type="text/css">
- @import url(https://fonts.googleapis.com/css?family=Open Sans);
- </style>
- <!--<![endif]-->
- <style type="text/css">
- @media only screen and (min-width: 480px) {
- .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- }
- </style>
- <style media="screen and (min-width:480px)">
- .moz-text-html .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- [owa] .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- @media only screen and (max-width: 480px) {
- table.mj-full-width-mobile {
- width: 100% !important;
- }
-
- td.mj-full-width-mobile {
- width: auto !important;
- }
- }
- </style>
- </head>
-
- <body style="word-spacing: normal; background-color: #f2f7fa">
- <div style="background-color: #f2f7fa">
- <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 0px 0px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <br />
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0;
- padding-bottom: 0px;
- padding-left: 0px;
- padding-right: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-right: 0px;
- padding-bottom: 40px;
- padding-left: 0px;
- word-break: break-word;
- "
- >
- <p
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 100%;
- "
- ></p>
- <!--[if mso | IE
- ]><table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 600px;
- "
- role="presentation"
- width="600px"
- >
- <tr>
- <td style="height: 0; line-height: 0">
- &nbsp;
- </td>
- </tr>
- </table><!
- [endif]-->
- </td>
- </tr>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="
- border-collapse: collapse;
- border-spacing: 0px;
- "
- >
- <tbody>
- <tr>
- <td style="width: 121px">
- <img
- alt=""
- height="auto"
- src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
- style="
- border: none;
- display: block;
- outline: none;
- text-decoration: none;
- height: auto;
- width: 100%;
- font-size: 13px;
- "
- width="121"
- />
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 70px;
- padding-top: 30px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
-
-<tbody>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <h1
- style="
- text-align: center;
- color: #000000;
- line-height: 32px;
- "
- >
- Vespa Cloud Notifications
- </h1>
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
-
-<p>
- There are problems with tests for default.default:
-</p>
-<p>Test package has production tests, but no production tests are declared in deployment.xml</p>
-<p>See <a href="https://docs.vespa.ai/en/testing.html">https://docs.vespa.ai/en/testing.html</a> for details on how to write system tests for Vespa</p>
-
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="center"
- vertical-align="middle"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 20px;
- padding-bottom: 20px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="border-collapse: separate; line-height: 100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- bgcolor="#005A8E"
- role="presentation"
- style="
- border: none;
- border-radius: 100px;
- cursor: auto;
- mso-padding-alt: 15px 25px 15px 25px;
- background: #005a8e;
- "
- valign="middle"
- >
- <a
- href="https://console.tld/tenant/tenant1/application/default/prod/instance/default"
- style="
- display: inline-block;
- background: #005a8e;
- color: #ffffff;
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- font-weight: normal;
- line-height: 120%;
- margin: 0;
- text-decoration: none;
- text-transform: none;
- padding: 15px 25px 15px 25px;
- mso-padding-alt: 0px;
- border-radius: 100px;
- "
- target="_blank"
- ><b style="font-weight: 700"
- ><b style="font-weight: 700"
- >Go to Console</b
- ></b
- ></a
- >
- </td>
- </tr>
- </tbody>
- </table>
- </td>
-</tr>
-</tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 20px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 0px 20px 0px 20px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: center;
- color: #797e82;
- "
- >
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"
- ><span style="color: #005a8e"
- >Yahoo Privacy Policy</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://console.tld/terms-of-service-trial.html"
- ><span style="color: #005a8e"
- >Terms of Service</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://console.tld/support"
- ><span style="color: #005a8e">Support</span></a
- >
- </p>
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: inherit; text-decoration: none"
- href="https://console.tld/tenant/tenant1/account/notifications"
- >Click
- <span style="color: #005a8e"><u>here</u></span>
- to manage your notifications setting.</a
- ><br />
- </p>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </div>
- </body>
-</html>
diff --git a/controller-server/src/test/resources/mail/trial-expired.html b/controller-server/src/test/resources/mail/trial-expired.html
deleted file mode 100644
index bdeafe8c7d3..00000000000
--- a/controller-server/src/test/resources/mail/trial-expired.html
+++ /dev/null
@@ -1,646 +0,0 @@
-<!DOCTYPE html>
-<html
- xmlns="http://www.w3.org/1999/xhtml"
- xmlns:v="urn:schemas-microsoft-com:vml"
- xmlns:o="urn:schemas-microsoft-com:office:office"
->
- <head>
- <title></title>
- <!--[if !mso]><!-->
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
- <!--<![endif]-->
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <style type="text/css">
- #outlook a {
- padding: 0;
- }
-
- body {
- margin: 0;
- padding: 0;
- -webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
- }
-
- table,
- td {
- border-collapse: collapse;
- mso-table-lspace: 0pt;
- mso-table-rspace: 0pt;
- }
-
- img {
- border: 0;
- height: auto;
- line-height: 100%;
- outline: none;
- text-decoration: none;
- -ms-interpolation-mode: bicubic;
- }
-
- p {
- display: block;
- margin: 13px 0;
- }
- </style>
- <!--[if mso]>
- <noscript>
- <xml>
- <o:OfficeDocumentSettings>
- <o:AllowPNG />
- <o:PixelsPerInch>96</o:PixelsPerInch>
- </o:OfficeDocumentSettings>
- </xml>
- </noscript>
- <![endif]-->
- <!--[if lte mso 11]>
- <style type="text/css">
- .mj-outlook-group-fix {
- width: 100% !important;
- }
- </style>
- <![endif]-->
- <!--[if !mso]><!-->
- <link
- href="https://fonts.googleapis.com/css?family=Open Sans"
- rel="stylesheet"
- type="text/css"
- />
- <style type="text/css">
- @import url(https://fonts.googleapis.com/css?family=Open Sans);
- </style>
- <!--<![endif]-->
- <style type="text/css">
- @media only screen and (min-width: 480px) {
- .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- }
- </style>
- <style media="screen and (min-width:480px)">
- .moz-text-html .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- [owa] .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- @media only screen and (max-width: 480px) {
- table.mj-full-width-mobile {
- width: 100% !important;
- }
-
- td.mj-full-width-mobile {
- width: auto !important;
- }
- }
- </style>
- </head>
-
- <body style="word-spacing: normal; background-color: #f2f7fa">
- <div style="background-color: #f2f7fa">
- <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 0px 0px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <br />
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0;
- padding-bottom: 0px;
- padding-left: 0px;
- padding-right: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-right: 0px;
- padding-bottom: 40px;
- padding-left: 0px;
- word-break: break-word;
- "
- >
- <p
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 100%;
- "
- ></p>
- <!--[if mso | IE
- ]><table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 600px;
- "
- role="presentation"
- width="600px"
- >
- <tr>
- <td style="height: 0; line-height: 0">
- &nbsp;
- </td>
- </tr>
- </table><!
- [endif]-->
- </td>
- </tr>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="
- border-collapse: collapse;
- border-spacing: 0px;
- "
- >
- <tbody>
- <tr>
- <td style="width: 121px">
- <img
- alt=""
- height="auto"
- src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
- style="
- border: none;
- display: block;
- outline: none;
- text-decoration: none;
- height: auto;
- width: 100%;
- font-size: 13px;
- "
- width="121"
- />
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 70px;
- padding-top: 30px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
-
-<tbody>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <h1
- style="
- text-align: center;
- color: #000000;
- line-height: 32px;
- "
- >
- Your Vespa Cloud trial has expired
- </h1>
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
-
-<p>
- Your Vespa Cloud trial has expired. Please reach out to us if you have any questions or feedback.
-</p>
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="center"
- vertical-align="middle"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 20px;
- padding-bottom: 20px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="border-collapse: separate; line-height: 100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- bgcolor="#005A8E"
- role="presentation"
- style="
- border: none;
- border-radius: 100px;
- cursor: auto;
- mso-padding-alt: 15px 25px 15px 25px;
- background: #005a8e;
- "
- valign="middle"
- >
- <a
- href="https://console.tld/tenant/trial-tenant"
- style="
- display: inline-block;
- background: #005a8e;
- color: #ffffff;
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- font-weight: normal;
- line-height: 120%;
- margin: 0;
- text-decoration: none;
- text-transform: none;
- padding: 15px 25px 15px 25px;
- mso-padding-alt: 0px;
- border-radius: 100px;
- "
- target="_blank"
- ><b style="font-weight: 700"
- ><b style="font-weight: 700"
- >Go to Console</b
- ></b
- ></a
- >
- </td>
- </tr>
- </tbody>
- </table>
- </td>
-</tr>
-</tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 20px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 0px 20px 0px 20px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: center;
- color: #797e82;
- "
- >
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"
- ><span style="color: #005a8e"
- >Yahoo Privacy Policy</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://console.tld/terms-of-service-trial.html"
- ><span style="color: #005a8e"
- >Terms of Service</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://console.tld/support"
- ><span style="color: #005a8e">Support</span></a
- >
- </p>
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: inherit; text-decoration: none"
- href="https://console.tld/tenant/trial-tenant/account/notifications"
- >Click
- <span style="color: #005a8e"><u>here</u></span>
- to manage your notifications setting.</a
- ><br />
- </p>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </div>
- </body>
-</html>
diff --git a/controller-server/src/test/resources/mail/trial-expires-immediately.html b/controller-server/src/test/resources/mail/trial-expires-immediately.html
deleted file mode 100644
index db89eca195a..00000000000
--- a/controller-server/src/test/resources/mail/trial-expires-immediately.html
+++ /dev/null
@@ -1,646 +0,0 @@
-<!DOCTYPE html>
-<html
- xmlns="http://www.w3.org/1999/xhtml"
- xmlns:v="urn:schemas-microsoft-com:vml"
- xmlns:o="urn:schemas-microsoft-com:office:office"
->
- <head>
- <title></title>
- <!--[if !mso]><!-->
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
- <!--<![endif]-->
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <style type="text/css">
- #outlook a {
- padding: 0;
- }
-
- body {
- margin: 0;
- padding: 0;
- -webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
- }
-
- table,
- td {
- border-collapse: collapse;
- mso-table-lspace: 0pt;
- mso-table-rspace: 0pt;
- }
-
- img {
- border: 0;
- height: auto;
- line-height: 100%;
- outline: none;
- text-decoration: none;
- -ms-interpolation-mode: bicubic;
- }
-
- p {
- display: block;
- margin: 13px 0;
- }
- </style>
- <!--[if mso]>
- <noscript>
- <xml>
- <o:OfficeDocumentSettings>
- <o:AllowPNG />
- <o:PixelsPerInch>96</o:PixelsPerInch>
- </o:OfficeDocumentSettings>
- </xml>
- </noscript>
- <![endif]-->
- <!--[if lte mso 11]>
- <style type="text/css">
- .mj-outlook-group-fix {
- width: 100% !important;
- }
- </style>
- <![endif]-->
- <!--[if !mso]><!-->
- <link
- href="https://fonts.googleapis.com/css?family=Open Sans"
- rel="stylesheet"
- type="text/css"
- />
- <style type="text/css">
- @import url(https://fonts.googleapis.com/css?family=Open Sans);
- </style>
- <!--<![endif]-->
- <style type="text/css">
- @media only screen and (min-width: 480px) {
- .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- }
- </style>
- <style media="screen and (min-width:480px)">
- .moz-text-html .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- [owa] .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- @media only screen and (max-width: 480px) {
- table.mj-full-width-mobile {
- width: 100% !important;
- }
-
- td.mj-full-width-mobile {
- width: auto !important;
- }
- }
- </style>
- </head>
-
- <body style="word-spacing: normal; background-color: #f2f7fa">
- <div style="background-color: #f2f7fa">
- <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 0px 0px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <br />
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0;
- padding-bottom: 0px;
- padding-left: 0px;
- padding-right: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-right: 0px;
- padding-bottom: 40px;
- padding-left: 0px;
- word-break: break-word;
- "
- >
- <p
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 100%;
- "
- ></p>
- <!--[if mso | IE
- ]><table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 600px;
- "
- role="presentation"
- width="600px"
- >
- <tr>
- <td style="height: 0; line-height: 0">
- &nbsp;
- </td>
- </tr>
- </table><!
- [endif]-->
- </td>
- </tr>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="
- border-collapse: collapse;
- border-spacing: 0px;
- "
- >
- <tbody>
- <tr>
- <td style="width: 121px">
- <img
- alt=""
- height="auto"
- src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
- style="
- border: none;
- display: block;
- outline: none;
- text-decoration: none;
- height: auto;
- width: 100%;
- font-size: 13px;
- "
- width="121"
- />
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 70px;
- padding-top: 30px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
-
-<tbody>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <h1
- style="
- text-align: center;
- color: #000000;
- line-height: 32px;
- "
- >
- Your Vespa Cloud trial expires tomorrow
- </h1>
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
-
-<p>
- Your Vespa Cloud trial expires tomorrow. Please reach out to us if you have any questions or feedback.
-</p>
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="center"
- vertical-align="middle"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 20px;
- padding-bottom: 20px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="border-collapse: separate; line-height: 100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- bgcolor="#005A8E"
- role="presentation"
- style="
- border: none;
- border-radius: 100px;
- cursor: auto;
- mso-padding-alt: 15px 25px 15px 25px;
- background: #005a8e;
- "
- valign="middle"
- >
- <a
- href="https://console.tld/tenant/trial-tenant"
- style="
- display: inline-block;
- background: #005a8e;
- color: #ffffff;
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- font-weight: normal;
- line-height: 120%;
- margin: 0;
- text-decoration: none;
- text-transform: none;
- padding: 15px 25px 15px 25px;
- mso-padding-alt: 0px;
- border-radius: 100px;
- "
- target="_blank"
- ><b style="font-weight: 700"
- ><b style="font-weight: 700"
- >Go to Console</b
- ></b
- ></a
- >
- </td>
- </tr>
- </tbody>
- </table>
- </td>
-</tr>
-</tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 20px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 0px 20px 0px 20px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: center;
- color: #797e82;
- "
- >
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"
- ><span style="color: #005a8e"
- >Yahoo Privacy Policy</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://console.tld/terms-of-service-trial.html"
- ><span style="color: #005a8e"
- >Terms of Service</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://console.tld/support"
- ><span style="color: #005a8e">Support</span></a
- >
- </p>
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: inherit; text-decoration: none"
- href="https://console.tld/tenant/trial-tenant/account/notifications"
- >Click
- <span style="color: #005a8e"><u>here</u></span>
- to manage your notifications setting.</a
- ><br />
- </p>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </div>
- </body>
-</html>
diff --git a/controller-server/src/test/resources/mail/trial-midway-checkin.html b/controller-server/src/test/resources/mail/trial-midway-checkin.html
deleted file mode 100644
index fbe0d573538..00000000000
--- a/controller-server/src/test/resources/mail/trial-midway-checkin.html
+++ /dev/null
@@ -1,646 +0,0 @@
-<!DOCTYPE html>
-<html
- xmlns="http://www.w3.org/1999/xhtml"
- xmlns:v="urn:schemas-microsoft-com:vml"
- xmlns:o="urn:schemas-microsoft-com:office:office"
->
- <head>
- <title></title>
- <!--[if !mso]><!-->
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
- <!--<![endif]-->
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <style type="text/css">
- #outlook a {
- padding: 0;
- }
-
- body {
- margin: 0;
- padding: 0;
- -webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
- }
-
- table,
- td {
- border-collapse: collapse;
- mso-table-lspace: 0pt;
- mso-table-rspace: 0pt;
- }
-
- img {
- border: 0;
- height: auto;
- line-height: 100%;
- outline: none;
- text-decoration: none;
- -ms-interpolation-mode: bicubic;
- }
-
- p {
- display: block;
- margin: 13px 0;
- }
- </style>
- <!--[if mso]>
- <noscript>
- <xml>
- <o:OfficeDocumentSettings>
- <o:AllowPNG />
- <o:PixelsPerInch>96</o:PixelsPerInch>
- </o:OfficeDocumentSettings>
- </xml>
- </noscript>
- <![endif]-->
- <!--[if lte mso 11]>
- <style type="text/css">
- .mj-outlook-group-fix {
- width: 100% !important;
- }
- </style>
- <![endif]-->
- <!--[if !mso]><!-->
- <link
- href="https://fonts.googleapis.com/css?family=Open Sans"
- rel="stylesheet"
- type="text/css"
- />
- <style type="text/css">
- @import url(https://fonts.googleapis.com/css?family=Open Sans);
- </style>
- <!--<![endif]-->
- <style type="text/css">
- @media only screen and (min-width: 480px) {
- .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- }
- </style>
- <style media="screen and (min-width:480px)">
- .moz-text-html .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- [owa] .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- @media only screen and (max-width: 480px) {
- table.mj-full-width-mobile {
- width: 100% !important;
- }
-
- td.mj-full-width-mobile {
- width: auto !important;
- }
- }
- </style>
- </head>
-
- <body style="word-spacing: normal; background-color: #f2f7fa">
- <div style="background-color: #f2f7fa">
- <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 0px 0px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <br />
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0;
- padding-bottom: 0px;
- padding-left: 0px;
- padding-right: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-right: 0px;
- padding-bottom: 40px;
- padding-left: 0px;
- word-break: break-word;
- "
- >
- <p
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 100%;
- "
- ></p>
- <!--[if mso | IE
- ]><table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 600px;
- "
- role="presentation"
- width="600px"
- >
- <tr>
- <td style="height: 0; line-height: 0">
- &nbsp;
- </td>
- </tr>
- </table><!
- [endif]-->
- </td>
- </tr>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="
- border-collapse: collapse;
- border-spacing: 0px;
- "
- >
- <tbody>
- <tr>
- <td style="width: 121px">
- <img
- alt=""
- height="auto"
- src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
- style="
- border: none;
- display: block;
- outline: none;
- text-decoration: none;
- height: auto;
- width: 100%;
- font-size: 13px;
- "
- width="121"
- />
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 70px;
- padding-top: 30px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
-
-<tbody>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <h1
- style="
- text-align: center;
- color: #000000;
- line-height: 32px;
- "
- >
- How is your Vespa Cloud trial going?
- </h1>
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
-
-<p>
- How is your Vespa Cloud trial going? Please reach out to us if you have any questions or feedback.
-</p>
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="center"
- vertical-align="middle"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 20px;
- padding-bottom: 20px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="border-collapse: separate; line-height: 100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- bgcolor="#005A8E"
- role="presentation"
- style="
- border: none;
- border-radius: 100px;
- cursor: auto;
- mso-padding-alt: 15px 25px 15px 25px;
- background: #005a8e;
- "
- valign="middle"
- >
- <a
- href="https://console.tld/tenant/trial-tenant"
- style="
- display: inline-block;
- background: #005a8e;
- color: #ffffff;
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- font-weight: normal;
- line-height: 120%;
- margin: 0;
- text-decoration: none;
- text-transform: none;
- padding: 15px 25px 15px 25px;
- mso-padding-alt: 0px;
- border-radius: 100px;
- "
- target="_blank"
- ><b style="font-weight: 700"
- ><b style="font-weight: 700"
- >Go to Console</b
- ></b
- ></a
- >
- </td>
- </tr>
- </tbody>
- </table>
- </td>
-</tr>
-</tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 20px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 0px 20px 0px 20px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: center;
- color: #797e82;
- "
- >
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"
- ><span style="color: #005a8e"
- >Yahoo Privacy Policy</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://console.tld/terms-of-service-trial.html"
- ><span style="color: #005a8e"
- >Terms of Service</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://console.tld/support"
- ><span style="color: #005a8e">Support</span></a
- >
- </p>
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: inherit; text-decoration: none"
- href="https://console.tld/tenant/trial-tenant/account/notifications"
- >Click
- <span style="color: #005a8e"><u>here</u></span>
- to manage your notifications setting.</a
- ><br />
- </p>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </div>
- </body>
-</html>
diff --git a/controller-server/src/test/resources/mail/trial-signed-up.html b/controller-server/src/test/resources/mail/trial-signed-up.html
deleted file mode 100644
index 2e652532db8..00000000000
--- a/controller-server/src/test/resources/mail/trial-signed-up.html
+++ /dev/null
@@ -1,646 +0,0 @@
-<!DOCTYPE html>
-<html
- xmlns="http://www.w3.org/1999/xhtml"
- xmlns:v="urn:schemas-microsoft-com:vml"
- xmlns:o="urn:schemas-microsoft-com:office:office"
->
- <head>
- <title></title>
- <!--[if !mso]><!-->
- <meta http-equiv="X-UA-Compatible" content="IE=edge" />
- <!--<![endif]-->
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <style type="text/css">
- #outlook a {
- padding: 0;
- }
-
- body {
- margin: 0;
- padding: 0;
- -webkit-text-size-adjust: 100%;
- -ms-text-size-adjust: 100%;
- }
-
- table,
- td {
- border-collapse: collapse;
- mso-table-lspace: 0pt;
- mso-table-rspace: 0pt;
- }
-
- img {
- border: 0;
- height: auto;
- line-height: 100%;
- outline: none;
- text-decoration: none;
- -ms-interpolation-mode: bicubic;
- }
-
- p {
- display: block;
- margin: 13px 0;
- }
- </style>
- <!--[if mso]>
- <noscript>
- <xml>
- <o:OfficeDocumentSettings>
- <o:AllowPNG />
- <o:PixelsPerInch>96</o:PixelsPerInch>
- </o:OfficeDocumentSettings>
- </xml>
- </noscript>
- <![endif]-->
- <!--[if lte mso 11]>
- <style type="text/css">
- .mj-outlook-group-fix {
- width: 100% !important;
- }
- </style>
- <![endif]-->
- <!--[if !mso]><!-->
- <link
- href="https://fonts.googleapis.com/css?family=Open Sans"
- rel="stylesheet"
- type="text/css"
- />
- <style type="text/css">
- @import url(https://fonts.googleapis.com/css?family=Open Sans);
- </style>
- <!--<![endif]-->
- <style type="text/css">
- @media only screen and (min-width: 480px) {
- .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- }
- </style>
- <style media="screen and (min-width:480px)">
- .moz-text-html .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- [owa] .mj-column-per-100 {
- width: 100% !important;
- max-width: 100%;
- }
- </style>
- <style type="text/css">
- @media only screen and (max-width: 480px) {
- table.mj-full-width-mobile {
- width: 100% !important;
- }
-
- td.mj-full-width-mobile {
- width: auto !important;
- }
- }
- </style>
- </head>
-
- <body style="word-spacing: normal; background-color: #f2f7fa">
- <div style="background-color: #f2f7fa">
- <!--[if mso | IE]><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 0px 0px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <br />
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0;
- padding-bottom: 0px;
- padding-left: 0px;
- padding-right: 0px;
- padding-top: 0px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-right: 0px;
- padding-bottom: 40px;
- padding-left: 0px;
- word-break: break-word;
- "
- >
- <p
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 100%;
- "
- ></p>
- <!--[if mso | IE
- ]><table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- style="
- border-top: solid 8px #005a8e;
- font-size: 1px;
- margin: 0px auto;
- width: 600px;
- "
- role="presentation"
- width="600px"
- >
- <tr>
- <td style="height: 0; line-height: 0">
- &nbsp;
- </td>
- </tr>
- </table><!
- [endif]-->
- </td>
- </tr>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="
- border-collapse: collapse;
- border-spacing: 0px;
- "
- >
- <tbody>
- <tr>
- <td style="width: 121px">
- <img
- alt=""
- height="auto"
- src="https://data.vespa.oath.cloud/assets/vespa-cloud-logo.png"
- style="
- border: none;
- display: block;
- outline: none;
- text-decoration: none;
- height: auto;
- width: 100%;
- font-size: 13px;
- "
- width="121"
- />
- </td>
- </tr>
- </tbody>
- </table>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" bgcolor="#ffffff" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div
- style="
- background: #ffffff;
- background-color: #ffffff;
- margin: 0px auto;
- max-width: 600px;
- "
- >
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="background: #ffffff; background-color: #ffffff; width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 70px;
- padding-top: 30px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
-
-<tbody>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
- <h1
- style="
- text-align: center;
- color: #000000;
- line-height: 32px;
- "
- >
- Welcome to Vespa Cloud
- </h1>
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="left"
- style="
- font-size: 0px;
- padding: 0px 25px 0px 25px;
- padding-top: 0px;
- padding-right: 50px;
- padding-bottom: 0px;
- padding-left: 50px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- line-height: 22px;
- text-align: left;
- color: #797e82;
- "
- >
-
-<p>
- Welcome to Vespa Cloud! We hope you will enjoy your trial. Please reach out to us if you have any questions or feedback.
-</p>
- </div>
- </td>
-</tr>
-<tr>
- <td
- align="center"
- vertical-align="middle"
- style="
- font-size: 0px;
- padding: 10px 25px;
- padding-top: 20px;
- padding-bottom: 20px;
- word-break: break-word;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="border-collapse: separate; line-height: 100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- bgcolor="#005A8E"
- role="presentation"
- style="
- border: none;
- border-radius: 100px;
- cursor: auto;
- mso-padding-alt: 15px 25px 15px 25px;
- background: #005a8e;
- "
- valign="middle"
- >
- <a
- href="https://console.tld/tenant/trial-tenant"
- style="
- display: inline-block;
- background: #005a8e;
- color: #ffffff;
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 13px;
- font-weight: normal;
- line-height: 120%;
- margin: 0;
- text-decoration: none;
- text-transform: none;
- padding: 15px 25px 15px 25px;
- mso-padding-alt: 0px;
- border-radius: 100px;
- "
- target="_blank"
- ><b style="font-weight: 700"
- ><b style="font-weight: 700"
- >Go to Console</b
- ></b
- ></a
- >
- </td>
- </tr>
- </tbody>
- </table>
- </td>
-</tr>
-</tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><table align="center" border="0" cellpadding="0" cellspacing="0" class="" role="presentation" style="width:600px;" width="600" ><tr><td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;"><![endif]-->
- <div style="margin: 0px auto; max-width: 600px">
- <table
- align="center"
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="width: 100%"
- >
- <tbody>
- <tr>
- <td
- style="
- direction: ltr;
- font-size: 0px;
- padding: 20px 0px 20px 0px;
- padding-bottom: 0px;
- padding-top: 20px;
- text-align: center;
- "
- >
- <!--[if mso | IE]><table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td class="" style="vertical-align:top;width:600px;" ><![endif]-->
- <div
- class="mj-column-per-100 mj-outlook-group-fix"
- style="
- font-size: 0px;
- text-align: left;
- direction: ltr;
- display: inline-block;
- vertical-align: top;
- width: 100%;
- "
- >
- <table
- border="0"
- cellpadding="0"
- cellspacing="0"
- role="presentation"
- style="vertical-align: top"
- width="100%"
- >
- <tbody>
- <tr>
- <td
- align="center"
- style="
- font-size: 0px;
- padding: 0px 20px 0px 20px;
- padding-top: 0px;
- padding-bottom: 0px;
- word-break: break-word;
- "
- >
- <div
- style="
- font-family: Open Sans, Helvetica, Arial,
- sans-serif;
- font-size: 11px;
- line-height: 22px;
- text-align: center;
- color: #797e82;
- "
- >
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://legal.yahoo.com/xw/en/yahoo/privacy/topic/b2bprivacypolicy/index.html"
- ><span style="color: #005a8e"
- >Yahoo Privacy Policy</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://console.tld/terms-of-service-trial.html"
- ><span style="color: #005a8e"
- >Terms of Service</span
- ></a
- ><span style="color: #797e82"
- >&nbsp; &nbsp;|&nbsp; &nbsp;</span
- ><a
- target="_blank"
- rel="noopener noreferrer"
- style="color: #005a8e"
- href="https://console.tld/support"
- ><span style="color: #005a8e">Support</span></a
- >
- </p>
- <p style="margin: 10px 0">
- <a
- target="_blank"
- rel="noopener noreferrer"
- style="color: inherit; text-decoration: none"
- href="https://console.tld/tenant/trial-tenant/account/notifications"
- >Click
- <span style="color: #005a8e"><u>here</u></span>
- to manage your notifications setting.</a
- ><br />
- </p>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <!--[if mso | IE]></td></tr></table><![endif]-->
- </div>
- </body>
-</html>
diff --git a/controller-server/src/test/resources/system-flags/existing-prod.us-east-3.json b/controller-server/src/test/resources/system-flags/existing-prod.us-east-3.json
deleted file mode 100644
index 8db6c423e4d..00000000000
--- a/controller-server/src/test/resources/system-flags/existing-prod.us-east-3.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-flag",
- "rules" : [
- {
- "value" : "prod.us-east-3.original"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/resources/system-flags/existing-prod.us-west-1.json b/controller-server/src/test/resources/system-flags/existing-prod.us-west-1.json
deleted file mode 100644
index 70fa0624a21..00000000000
--- a/controller-server/src/test/resources/system-flags/existing-prod.us-west-1.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-flag",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/resources/system-flags/flags/my-flag/main.json b/controller-server/src/test/resources/system-flags/flags/my-flag/main.json
deleted file mode 100644
index 70fa0624a21..00000000000
--- a/controller-server/src/test/resources/system-flags/flags/my-flag/main.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-flag",
- "rules" : [
- {
- "value" : "default"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/resources/system-flags/flags/my-flag/main.prod.us-east-3.json b/controller-server/src/test/resources/system-flags/flags/my-flag/main.prod.us-east-3.json
deleted file mode 100644
index e7e8a318a6f..00000000000
--- a/controller-server/src/test/resources/system-flags/flags/my-flag/main.prod.us-east-3.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "id" : "my-flag",
- "rules" : [
- {
- "value" : "us-east-3"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/resources/system-flags/partial/default.json b/controller-server/src/test/resources/system-flags/partial/default.json
deleted file mode 100644
index 881d4170c3b..00000000000
--- a/controller-server/src/test/resources/system-flags/partial/default.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "id" : "my-flag",
- "rules" : [
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "system",
- "values": [ "main" ]
- },
- {
- "type": "whitelist",
- "dimension": "cloud",
- "values": [ "aws" ]
- }
- ],
- "value" : "foo-value"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/resources/system-flags/partial/initial.json b/controller-server/src/test/resources/system-flags/partial/initial.json
deleted file mode 100644
index a16ea583005..00000000000
--- a/controller-server/src/test/resources/system-flags/partial/initial.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "id" : "my-flag",
- "rules" : [
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "application",
- "values": [ "a:b:c" ]
- }
- ],
- "value" : "bar-value"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/resources/system-flags/partial/put-controller.json b/controller-server/src/test/resources/system-flags/partial/put-controller.json
deleted file mode 100644
index 47aa0af47ce..00000000000
--- a/controller-server/src/test/resources/system-flags/partial/put-controller.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "id" : "my-flag",
- "rules" : [
- {
- "conditions": [
- {
- "type": "whitelist",
- "dimension": "cloud",
- "values": [ "aws" ]
- }
- ],
- "value" : "foo-value"
- }
- ]
-} \ No newline at end of file
diff --git a/controller-server/src/test/resources/testConfig.json b/controller-server/src/test/resources/testConfig.json
deleted file mode 100644
index 0ea4b163992..00000000000
--- a/controller-server/src/test/resources/testConfig.json
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "application": "tenant:application:default",
- "zone": "test.us-east-1",
- "system": "publiccd",
- "isCI": true,
- "platform": "1.2.3",
- "revision": 321,
- "deployedAt": 222,
- "endpoints": {
- "test.us-east-1": [
- "https://ai.default.default.global.vespa.oath.cloud/"
- ]
- },
- "zoneEndpoints": {
- "test.us-east-1": {
- "ai": "https://ai.default.default.global.vespa.oath.cloud/"
- }
- },
- "clusters": {
- "test.us-east-1": [
- "facts"
- ]
- }
-}
diff --git a/controller-server/src/test/resources/test_runner_services.xml-cd b/controller-server/src/test/resources/test_runner_services.xml-cd
deleted file mode 100644
index 35ad0d31577..00000000000
--- a/controller-server/src/test/resources/test_runner_services.xml-cd
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<services xmlns:deploy='vespa' version='1.0'>
- <container version='1.0' id='tester'>
-
- <component id="com.yahoo.vespa.hosted.testrunner.TestRunner" bundle="vespa-testrunner-components">
- <config name="com.yahoo.vespa.hosted.testrunner.test-runner">
- <artifactsPath>artifacts</artifactsPath>
- <surefireMemoryMb>5120</surefireMemoryMb>
- <useAthenzCredentials>true</useAthenzCredentials>
- <useTesterCertificate>false</useTesterCertificate>
- </config>
- </component>
-
- <handler id="com.yahoo.vespa.testrunner.TestRunnerHandler" bundle="vespa-osgi-testrunner">
- <binding>http://*/tester/v1/*</binding>
- </handler>
-
- <component id="ai.vespa.hosted.cd.cloud.impl.VespaTestRuntimeProvider" bundle="cloud-tenant-cd" />
-
- <component id="com.yahoo.vespa.testrunner.JunitRunner" bundle="vespa-osgi-testrunner">
- <config name="com.yahoo.vespa.testrunner.junit-test-runner">
- <artifactsPath>artifacts</artifactsPath>
- <useAthenzCredentials>true</useAthenzCredentials>
- </config>
- </component>
-
- <component id="com.yahoo.vespa.testrunner.VespaCliTestRunner" bundle="vespa-osgi-testrunner">
- <config name="com.yahoo.vespa.testrunner.vespa-cli-test-runner">
- <artifactsPath>artifacts</artifactsPath>
- <testsPath>tests</testsPath>
- <useAthenzCredentials>true</useAthenzCredentials>
- </config>
- </component>
-
- <nodes count="1">
- <resources vcpu="2.00" memory="12.00Gb" disk="75.00Gb" disk-speed="fast" storage-type="local" architecture="any"/>
- </nodes>
- </container>
-</services>
diff --git a/controller-server/src/test/resources/test_runner_services_with_legacy_tests.xml-cd b/controller-server/src/test/resources/test_runner_services_with_legacy_tests.xml-cd
deleted file mode 100644
index 91317f1490c..00000000000
--- a/controller-server/src/test/resources/test_runner_services_with_legacy_tests.xml-cd
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<services xmlns:deploy='vespa' version='1.0'>
- <container version='1.0' id='tester'>
-
- <component id="com.yahoo.vespa.hosted.testrunner.TestRunner" bundle="vespa-testrunner-components">
- <config name="com.yahoo.vespa.hosted.testrunner.test-runner">
- <artifactsPath>artifacts</artifactsPath>
- <surefireMemoryMb>5120</surefireMemoryMb>
- <useAthenzCredentials>true</useAthenzCredentials>
- <useTesterCertificate>false</useTesterCertificate>
- </config>
- </component>
-
- <handler id="com.yahoo.vespa.testrunner.TestRunnerHandler" bundle="vespa-osgi-testrunner">
- <binding>http://*/tester/v1/*</binding>
- </handler>
-
- <component id="ai.vespa.hosted.cd.cloud.impl.VespaTestRuntimeProvider" bundle="cloud-tenant-cd" />
-
- <component id="com.yahoo.vespa.testrunner.JunitRunner" bundle="vespa-osgi-testrunner">
- <config name="com.yahoo.vespa.testrunner.junit-test-runner">
- <artifactsPath>artifacts</artifactsPath>
- <useAthenzCredentials>true</useAthenzCredentials>
- </config>
- </component>
-
- <component id="com.yahoo.vespa.testrunner.VespaCliTestRunner" bundle="vespa-osgi-testrunner">
- <config name="com.yahoo.vespa.testrunner.vespa-cli-test-runner">
- <artifactsPath>artifacts</artifactsPath>
- <testsPath>tests</testsPath>
- <useAthenzCredentials>true</useAthenzCredentials>
- </config>
- </component>
-
- <nodes count="1">
- <jvm allocated-memory="17%"/>
- <resources vcpu="2.00" memory="12.00Gb" disk="75.00Gb" disk-speed="fast" storage-type="local" architecture="any"/>
- </nodes>
- </container>
-</services>
diff --git a/pom.xml b/pom.xml
index 65904ae3dd0..4e021fde2e3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,8 +62,6 @@
<module>container-search</module>
<module>container-spifly</module>
<module>container-test</module>
- <module>controller-api</module>
- <module>controller-server</module>
<module>defaults</module>
<module>dependency-versions</module>
<module>docproc</module>
diff --git a/screwdriver.yaml b/screwdriver.yaml
index e1ee1bcff8c..d4e50f5d581 100644
--- a/screwdriver.yaml
+++ b/screwdriver.yaml
@@ -595,6 +595,6 @@ jobs:
--typhoeus '{"connecttimeout": 10, "timeout": 30, "followlocation": false}' \
--hydra '{"max_concurrency": 1}' \
--ignore-urls '/slack.vespa.ai/,/localhost:8080/,/127.0.0.1:3000/,/favicon.svg/,/main.jsx/' \
- --ignore-files '/fnet/index.html/,/client/js/app/node_modules/,/controller-server/src/test/resources/mail/' \
+ --ignore-files '/fnet/index.html/,/client/js/app/node_modules/' \
--swap-urls '(.*).md:\1.html' \
_site
diff --git a/vespa-dependencies-enforcer/allowed-maven-dependencies.txt b/vespa-dependencies-enforcer/allowed-maven-dependencies.txt
index 844c0adabc0..1ca890eb2a8 100644
--- a/vespa-dependencies-enforcer/allowed-maven-dependencies.txt
+++ b/vespa-dependencies-enforcer/allowed-maven-dependencies.txt
@@ -20,7 +20,6 @@ com.fasterxml.jackson.core:jackson-databind:${jackson-databind.vespa.version}
com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:${jackson2.vespa.version}
com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${jackson2.vespa.version}
com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jackson2.vespa.version}
-com.github.cliftonlabs:json-simple:${findbugs.vespa.version}
com.github.luben:zstd-jni:${luben.zstd.vespa.version}
com.github.spotbugs:spotbugs-annotations:3.1.9
com.google.code.findbugs:jsr305:${findbugs.vespa.version}
@@ -44,11 +43,8 @@ com.yahoo.athenz:athenz-zms-core:${athenz.vespa.version}
com.yahoo.athenz:athenz-zpe-java-client:${athenz.vespa.version}
com.yahoo.athenz:athenz-zts-core:${athenz.vespa.version}
com.yahoo.rdl:rdl-java:1.5.4
-commons-beanutils:commons-beanutils:${commons-beanutils.vespa.version}
commons-cli:commons-cli:${commons-cli.vespa.version}
commons-codec:commons-codec:${commons-codec.vespa.version}
-commons-collections:commons-collections:${commons-collections.vespa.version}
-commons-fileupload:commons-fileupload:1.5
commons-io:commons-io:${commons-io.vespa.version}
commons-logging:commons-logging:${commons-logging.vespa.version}
io.airlift:aircompressor:${aircompressor.vespa.version}
@@ -90,8 +86,6 @@ org.antlr:antlr-runtime:${antlr.vespa.version}
org.antlr:antlr4-runtime:${antlr4.vespa.version}
org.apache.aries.spifly:org.apache.aries.spifly.dynamic.bundle:${spifly.vespa.version}
org.apache.commons:commons-compress:${commons-compress.vespa.version}
-org.apache.commons:commons-csv:${commons-csv.vespa.version}
-org.apache.commons:commons-digester3:${commons-digester.vespa.version}
org.apache.commons:commons-exec:${commons-exec.vespa.version}
org.apache.commons:commons-lang3:${commons-lang3.vespa.version}
org.apache.commons:commons-math3:${commons.math3.vespa.version}
@@ -124,7 +118,6 @@ org.apache.maven:maven-project:2.2.1
org.apache.maven:maven-repository-metadata:${maven-core.vespa.version}
org.apache.maven:maven-settings:${maven-core.vespa.version}
org.apache.opennlp:opennlp-tools:${opennlp.vespa.version}
-org.apache.velocity.tools:velocity-tools-generic:${velocity.tools.vespa.version}
org.apache.velocity:velocity-engine-core:${velocity.vespa.version}
org.apache.yetus:audience-annotations:0.12.0
org.apache.zookeeper:zookeeper-jute:${zookeeper.client.vespa.version}